mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 11:41:44 +10:00
Merge branch 'main' into feat/signing-reminders
This commit is contained in:
@ -0,0 +1,34 @@
|
||||
import { OrganisationType } from '@prisma/client';
|
||||
|
||||
import { createOrganisation } from '@documenso/lib/server-only/organisation/create-organisation';
|
||||
import { internalClaims } from '@documenso/lib/types/subscription';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZCreateAdminOrganisationRequestSchema,
|
||||
ZCreateAdminOrganisationResponseSchema,
|
||||
} from './create-admin-organisation.types';
|
||||
|
||||
export const createAdminOrganisationRoute = adminProcedure
|
||||
.input(ZCreateAdminOrganisationRequestSchema)
|
||||
.output(ZCreateAdminOrganisationResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { ownerUserId, data } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
ownerUserId,
|
||||
},
|
||||
});
|
||||
|
||||
const organisation = await createOrganisation({
|
||||
userId: ownerUserId,
|
||||
name: data.name,
|
||||
type: OrganisationType.ORGANISATION,
|
||||
claim: internalClaims.free,
|
||||
});
|
||||
|
||||
return {
|
||||
organisationId: organisation.id,
|
||||
};
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZOrganisationNameSchema } from '../organisation-router/create-organisation.types';
|
||||
|
||||
export const ZCreateAdminOrganisationRequestSchema = z.object({
|
||||
ownerUserId: z.number(),
|
||||
data: z.object({
|
||||
name: ZOrganisationNameSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZCreateAdminOrganisationResponseSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
});
|
||||
|
||||
export type TCreateAdminOrganisationRequest = z.infer<typeof ZCreateAdminOrganisationRequestSchema>;
|
||||
56
packages/trpc/server/admin-router/create-stripe-customer.ts
Normal file
56
packages/trpc/server/admin-router/create-stripe-customer.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZCreateStripeCustomerRequestSchema,
|
||||
ZCreateStripeCustomerResponseSchema,
|
||||
} from './create-stripe-customer.types';
|
||||
|
||||
export const createStripeCustomerRoute = adminProcedure
|
||||
.input(ZCreateStripeCustomerRequestSchema)
|
||||
.output(ZCreateStripeCustomerResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { organisationId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
const organisation = await prisma.organisation.findUnique({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
include: {
|
||||
owner: {
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const stripeCustomer = await createCustomer({
|
||||
name: organisation.name,
|
||||
email: organisation.owner.email,
|
||||
});
|
||||
|
||||
await tx.organisation.update({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
data: {
|
||||
customerId: stripeCustomer.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZCreateStripeCustomerRequestSchema = z.object({
|
||||
organisationId: z.string().describe('The organisation to attach the customer to'),
|
||||
});
|
||||
|
||||
export const ZCreateStripeCustomerResponseSchema = z.void();
|
||||
|
||||
export type TCreateStripeCustomerRequest = z.infer<typeof ZCreateStripeCustomerRequestSchema>;
|
||||
@ -0,0 +1,27 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZCreateSubscriptionClaimRequestSchema,
|
||||
ZCreateSubscriptionClaimResponseSchema,
|
||||
} from './create-subscription-claim.types';
|
||||
|
||||
export const createSubscriptionClaimRoute = adminProcedure
|
||||
.input(ZCreateSubscriptionClaimRequestSchema)
|
||||
.output(ZCreateSubscriptionClaimResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { name, teamCount, memberCount, flags } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input,
|
||||
});
|
||||
|
||||
await prisma.subscriptionClaim.create({
|
||||
data: {
|
||||
name,
|
||||
teamCount,
|
||||
memberCount,
|
||||
flags,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZClaimFlagsSchema } from '@documenso/lib/types/subscription';
|
||||
|
||||
export const ZCreateSubscriptionClaimRequestSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
teamCount: z.number().int().min(0),
|
||||
memberCount: z.number().int().min(0),
|
||||
flags: ZClaimFlagsSchema,
|
||||
});
|
||||
|
||||
export const ZCreateSubscriptionClaimResponseSchema = z.void();
|
||||
|
||||
export type TCreateSubscriptionClaimRequest = z.infer<typeof ZCreateSubscriptionClaimRequestSchema>;
|
||||
@ -0,0 +1,43 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZDeleteSubscriptionClaimRequestSchema,
|
||||
ZDeleteSubscriptionClaimResponseSchema,
|
||||
} from './delete-subscription-claim.types';
|
||||
|
||||
export const deleteSubscriptionClaimRoute = adminProcedure
|
||||
.input(ZDeleteSubscriptionClaimRequestSchema)
|
||||
.output(ZDeleteSubscriptionClaimResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
const existingClaim = await prisma.subscriptionClaim.findFirst({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingClaim) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Subscription claim not found' });
|
||||
}
|
||||
|
||||
if (existingClaim.locked) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Cannot delete locked subscription claim',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.subscriptionClaim.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZDeleteSubscriptionClaimRequestSchema = z.object({
|
||||
id: z.string().cuid(),
|
||||
});
|
||||
|
||||
export const ZDeleteSubscriptionClaimResponseSchema = z.void();
|
||||
|
||||
export type TDeleteSubscriptionClaimRequest = z.infer<typeof ZDeleteSubscriptionClaimRequestSchema>;
|
||||
160
packages/trpc/server/admin-router/find-admin-organisations.ts
Normal file
160
packages/trpc/server/admin-router/find-admin-organisations.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import type { FindResultResponse } from '@documenso/lib/types/search-params';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZFindAdminOrganisationsRequestSchema,
|
||||
ZFindAdminOrganisationsResponseSchema,
|
||||
} from './find-admin-organisations.types';
|
||||
|
||||
export const findAdminOrganisationsRoute = adminProcedure
|
||||
.input(ZFindAdminOrganisationsRequestSchema)
|
||||
.output(ZFindAdminOrganisationsResponseSchema)
|
||||
.query(async ({ input }) => {
|
||||
const { query, page, perPage, ownerUserId, memberUserId } = input;
|
||||
|
||||
return await findAdminOrganisations({
|
||||
query,
|
||||
page,
|
||||
perPage,
|
||||
ownerUserId,
|
||||
memberUserId,
|
||||
});
|
||||
});
|
||||
|
||||
type FindAdminOrganisationsOptions = {
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
ownerUserId?: number;
|
||||
memberUserId?: number;
|
||||
};
|
||||
|
||||
export const findAdminOrganisations = async ({
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
ownerUserId,
|
||||
memberUserId,
|
||||
}: FindAdminOrganisationsOptions) => {
|
||||
let whereClause: Prisma.OrganisationWhereInput = {};
|
||||
|
||||
if (query) {
|
||||
whereClause = {
|
||||
OR: [
|
||||
{
|
||||
id: {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
{
|
||||
owner: {
|
||||
email: {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
customerId: {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (query && query.startsWith('claim:')) {
|
||||
whereClause = {
|
||||
organisationClaim: {
|
||||
originalSubscriptionClaimId: {
|
||||
contains: query.slice(6),
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (query && query.startsWith('org_')) {
|
||||
whereClause = {
|
||||
OR: [
|
||||
{
|
||||
id: {
|
||||
equals: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
{
|
||||
url: {
|
||||
equals: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (ownerUserId) {
|
||||
whereClause = {
|
||||
...whereClause,
|
||||
ownerUserId,
|
||||
};
|
||||
}
|
||||
|
||||
if (memberUserId) {
|
||||
whereClause = {
|
||||
...whereClause,
|
||||
members: {
|
||||
some: { userId: memberUserId },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.organisation.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
name: true,
|
||||
url: true,
|
||||
customerId: true,
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
subscription: true,
|
||||
},
|
||||
}),
|
||||
prisma.organisation.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
@ -0,0 +1,42 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import OrganisationSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
|
||||
import SubscriptionSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
|
||||
import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
|
||||
export const ZFindAdminOrganisationsRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
ownerUserId: z.number().optional(),
|
||||
memberUserId: z.number().optional(),
|
||||
});
|
||||
|
||||
export const ZFindAdminOrganisationsResponseSchema = ZFindResultResponse.extend({
|
||||
data: OrganisationSchema.pick({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
name: true,
|
||||
url: true,
|
||||
customerId: true,
|
||||
})
|
||||
.extend({
|
||||
owner: UserSchema.pick({
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
}),
|
||||
subscription: SubscriptionSchema.pick({
|
||||
status: true,
|
||||
id: true,
|
||||
planId: true,
|
||||
priceId: true,
|
||||
periodEnd: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
cancelAtPeriodEnd: true,
|
||||
}).nullable(),
|
||||
})
|
||||
.array(),
|
||||
});
|
||||
|
||||
export type TFindAdminOrganisationsResponse = z.infer<typeof ZFindAdminOrganisationsResponseSchema>;
|
||||
@ -0,0 +1,76 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import type { FindResultResponse } from '@documenso/lib/types/search-params';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type SubscriptionClaimSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionClaimSchema';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZFindSubscriptionClaimsRequestSchema,
|
||||
ZFindSubscriptionClaimsResponseSchema,
|
||||
} from './find-subscription-claims.types';
|
||||
|
||||
export const findSubscriptionClaimsRoute = adminProcedure
|
||||
.input(ZFindSubscriptionClaimsRequestSchema)
|
||||
.output(ZFindSubscriptionClaimsResponseSchema)
|
||||
.query(async ({ input }) => {
|
||||
const { query, page, perPage } = input;
|
||||
|
||||
return await findSubscriptionClaims({ query, page, perPage });
|
||||
});
|
||||
|
||||
type FindSubscriptionClaimsOptions = {
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
};
|
||||
|
||||
export const findSubscriptionClaims = async ({
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 50,
|
||||
}: FindSubscriptionClaimsOptions) => {
|
||||
let whereClause: Prisma.SubscriptionClaimWhereInput = {};
|
||||
|
||||
if (query) {
|
||||
whereClause = {
|
||||
OR: [
|
||||
{
|
||||
id: {
|
||||
contains: query,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: {
|
||||
contains: query,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.subscriptionClaim.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
}),
|
||||
prisma.subscriptionClaim.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<z.infer<typeof SubscriptionClaimSchema>[]>;
|
||||
};
|
||||
@ -0,0 +1,22 @@
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import SubscriptionClaimSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionClaimSchema';
|
||||
|
||||
export const ZFindSubscriptionClaimsRequestSchema = ZFindSearchParamsSchema.extend({});
|
||||
|
||||
export const ZFindSubscriptionClaimsResponseSchema = ZFindResultResponse.extend({
|
||||
data: SubscriptionClaimSchema.pick({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
name: true,
|
||||
teamCount: true,
|
||||
memberCount: true,
|
||||
locked: true,
|
||||
flags: true,
|
||||
}).array(),
|
||||
});
|
||||
|
||||
export type TFindSubscriptionClaimsRequest = z.infer<typeof ZFindSubscriptionClaimsRequestSchema>;
|
||||
export type TFindSubscriptionClaimsResponse = z.infer<typeof ZFindSubscriptionClaimsResponseSchema>;
|
||||
62
packages/trpc/server/admin-router/get-admin-organisation.ts
Normal file
62
packages/trpc/server/admin-router/get-admin-organisation.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetAdminOrganisationRequestSchema,
|
||||
ZGetAdminOrganisationResponseSchema,
|
||||
} from './get-admin-organisation.types';
|
||||
|
||||
export const getAdminOrganisationRoute = adminProcedure
|
||||
.input(ZGetAdminOrganisationRequestSchema)
|
||||
.output(ZGetAdminOrganisationResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { organisationId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
return await getAdminOrganisation({
|
||||
organisationId,
|
||||
});
|
||||
});
|
||||
|
||||
type GetOrganisationOptions = {
|
||||
organisationId: string;
|
||||
};
|
||||
|
||||
export const getAdminOrganisation = async ({ organisationId }: GetOrganisationOptions) => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
include: {
|
||||
organisationClaim: true,
|
||||
organisationGlobalSettings: true,
|
||||
teams: true,
|
||||
members: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
subscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Organisation not found',
|
||||
});
|
||||
}
|
||||
|
||||
return organisation;
|
||||
};
|
||||
@ -0,0 +1,38 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZOrganisationSchema } from '@documenso/lib/types/organisation';
|
||||
import OrganisationClaimSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationClaimSchema';
|
||||
import OrganisationGlobalSettingsSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationGlobalSettingsSchema';
|
||||
import OrganisationMemberSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberSchema';
|
||||
import SubscriptionSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
|
||||
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
|
||||
export const ZGetAdminOrganisationRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
});
|
||||
|
||||
export const ZGetAdminOrganisationResponseSchema = ZOrganisationSchema.extend({
|
||||
organisationGlobalSettings: OrganisationGlobalSettingsSchema,
|
||||
teams: z.array(
|
||||
TeamSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
createdAt: true,
|
||||
avatarImageId: true,
|
||||
organisationId: true,
|
||||
}),
|
||||
),
|
||||
members: OrganisationMemberSchema.extend({
|
||||
user: UserSchema.pick({
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
}),
|
||||
}).array(),
|
||||
subscription: SubscriptionSchema.nullable(),
|
||||
organisationClaim: OrganisationClaimSchema,
|
||||
});
|
||||
|
||||
export type TGetAdminOrganisationResponse = z.infer<typeof ZGetAdminOrganisationResponseSchema>;
|
||||
@ -0,0 +1,50 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZResetTwoFactorRequestSchema,
|
||||
ZResetTwoFactorResponseSchema,
|
||||
} from './reset-two-factor-authentication.types';
|
||||
|
||||
export const resetTwoFactorRoute = adminProcedure
|
||||
.input(ZResetTwoFactorRequestSchema)
|
||||
.output(ZResetTwoFactorResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { userId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
return await resetTwoFactor({ userId });
|
||||
});
|
||||
|
||||
export type ResetTwoFactorOptions = {
|
||||
userId: number;
|
||||
};
|
||||
|
||||
export const resetTwoFactor = async ({ userId }: ResetTwoFactorOptions) => {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'User not found' });
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
twoFactorEnabled: false,
|
||||
twoFactorBackupCodes: null,
|
||||
twoFactorSecret: null,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZResetTwoFactorRequestSchema = z.object({
|
||||
userId: z.number(),
|
||||
});
|
||||
|
||||
export const ZResetTwoFactorResponseSchema = z.void();
|
||||
|
||||
export type TResetTwoFactorRequest = z.infer<typeof ZResetTwoFactorRequestSchema>;
|
||||
export type TResetTwoFactorResponse = z.infer<typeof ZResetTwoFactorResponseSchema>;
|
||||
@ -14,6 +14,14 @@ import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
|
||||
import { adminProcedure, router } from '../trpc';
|
||||
import { createAdminOrganisationRoute } from './create-admin-organisation';
|
||||
import { createStripeCustomerRoute } from './create-stripe-customer';
|
||||
import { createSubscriptionClaimRoute } from './create-subscription-claim';
|
||||
import { deleteSubscriptionClaimRoute } from './delete-subscription-claim';
|
||||
import { findAdminOrganisationsRoute } from './find-admin-organisations';
|
||||
import { findSubscriptionClaimsRoute } from './find-subscription-claims';
|
||||
import { getAdminOrganisationRoute } from './get-admin-organisation';
|
||||
import { resetTwoFactorRoute } from './reset-two-factor-authentication';
|
||||
import {
|
||||
ZAdminDeleteDocumentMutationSchema,
|
||||
ZAdminDeleteUserMutationSchema,
|
||||
@ -25,8 +33,30 @@ import {
|
||||
ZAdminUpdateRecipientMutationSchema,
|
||||
ZAdminUpdateSiteSettingMutationSchema,
|
||||
} from './schema';
|
||||
import { updateAdminOrganisationRoute } from './update-admin-organisation';
|
||||
import { updateSubscriptionClaimRoute } from './update-subscription-claim';
|
||||
|
||||
export const adminRouter = router({
|
||||
organisation: {
|
||||
find: findAdminOrganisationsRoute,
|
||||
get: getAdminOrganisationRoute,
|
||||
create: createAdminOrganisationRoute,
|
||||
update: updateAdminOrganisationRoute,
|
||||
},
|
||||
claims: {
|
||||
find: findSubscriptionClaimsRoute,
|
||||
create: createSubscriptionClaimRoute,
|
||||
update: updateSubscriptionClaimRoute,
|
||||
delete: deleteSubscriptionClaimRoute,
|
||||
},
|
||||
stripe: {
|
||||
createCustomer: createStripeCustomerRoute,
|
||||
},
|
||||
user: {
|
||||
resetTwoFactor: resetTwoFactorRoute,
|
||||
},
|
||||
|
||||
// Todo: migrate old routes
|
||||
findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => {
|
||||
const { query, page, perPage } = input;
|
||||
|
||||
@ -35,17 +65,30 @@ export const adminRouter = router({
|
||||
|
||||
updateUser: adminProcedure
|
||||
.input(ZAdminUpdateProfileMutationSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id, name, email, roles } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
roles,
|
||||
},
|
||||
});
|
||||
|
||||
return await updateUser({ id, name, email, roles });
|
||||
}),
|
||||
|
||||
updateRecipient: adminProcedure
|
||||
.input(ZAdminUpdateRecipientMutationSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id, name, email } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
return await updateRecipient({ id, name, email });
|
||||
}),
|
||||
|
||||
@ -54,6 +97,12 @@ export const adminRouter = router({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id, enabled, data } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
return await upsertSiteSetting({
|
||||
id,
|
||||
enabled,
|
||||
@ -64,9 +113,15 @@ export const adminRouter = router({
|
||||
|
||||
resealDocument: adminProcedure
|
||||
.input(ZAdminResealDocumentMutationSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
const document = await getEntireDocument({ id });
|
||||
|
||||
const isResealing = isDocumentCompleted(document.status);
|
||||
@ -74,44 +129,75 @@ export const adminRouter = router({
|
||||
return await sealDocument({ documentId: id, isResealing });
|
||||
}),
|
||||
|
||||
enableUser: adminProcedure.input(ZAdminEnableUserMutationSchema).mutation(async ({ input }) => {
|
||||
const { id } = input;
|
||||
enableUser: adminProcedure
|
||||
.input(ZAdminEnableUserMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
|
||||
const user = await getUserById({ id }).catch(() => null);
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await enableUser({ id });
|
||||
}),
|
||||
const user = await getUserById({ id }).catch(() => null);
|
||||
|
||||
disableUser: adminProcedure.input(ZAdminDisableUserMutationSchema).mutation(async ({ input }) => {
|
||||
const { id } = input;
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
const user = await getUserById({ id }).catch(() => null);
|
||||
return await enableUser({ id });
|
||||
}),
|
||||
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
disableUser: adminProcedure
|
||||
.input(ZAdminDisableUserMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await disableUser({ id });
|
||||
}),
|
||||
const user = await getUserById({ id }).catch(() => null);
|
||||
|
||||
deleteUser: adminProcedure.input(ZAdminDeleteUserMutationSchema).mutation(async ({ input }) => {
|
||||
const { id } = input;
|
||||
if (!user) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
return await deleteUser({ id });
|
||||
}),
|
||||
return await disableUser({ id });
|
||||
}),
|
||||
|
||||
deleteUser: adminProcedure
|
||||
.input(ZAdminDeleteUserMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
return await deleteUser({ id });
|
||||
}),
|
||||
|
||||
deleteDocument: adminProcedure
|
||||
.input(ZAdminDeleteDocumentMutationSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id, reason } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
await sendDeleteEmail({ documentId: id, reason });
|
||||
|
||||
return await superDeleteDocument({
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateAdminOrganisationRequestSchema,
|
||||
ZUpdateAdminOrganisationResponseSchema,
|
||||
} from './update-admin-organisation.types';
|
||||
|
||||
export const updateAdminOrganisationRoute = adminProcedure
|
||||
.input(ZUpdateAdminOrganisationRequestSchema)
|
||||
.output(ZUpdateAdminOrganisationResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { organisationId, data } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
const organisation = await prisma.organisation.findUnique({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
include: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const { name, url, customerId, claims, originalSubscriptionClaimId } = data;
|
||||
|
||||
await prisma.organisation.update({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
url,
|
||||
customerId: customerId ? customerId : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.organisationClaim.update({
|
||||
where: {
|
||||
id: organisation.organisationClaimId,
|
||||
},
|
||||
data: {
|
||||
...claims,
|
||||
originalSubscriptionClaimId,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZOrganisationNameSchema } from '../organisation-router/create-organisation.types';
|
||||
import { ZTeamUrlSchema } from '../team-router/schema';
|
||||
import { ZCreateSubscriptionClaimRequestSchema } from './create-subscription-claim.types';
|
||||
|
||||
export const ZUpdateAdminOrganisationRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
data: z.object({
|
||||
name: ZOrganisationNameSchema.optional(),
|
||||
url: ZTeamUrlSchema.optional(),
|
||||
claims: ZCreateSubscriptionClaimRequestSchema.pick({
|
||||
teamCount: true,
|
||||
memberCount: true,
|
||||
flags: true,
|
||||
}).optional(),
|
||||
customerId: z.string().optional(),
|
||||
originalSubscriptionClaimId: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZUpdateAdminOrganisationResponseSchema = z.void();
|
||||
|
||||
export type TUpdateAdminOrganisationRequest = z.infer<typeof ZUpdateAdminOrganisationRequestSchema>;
|
||||
@ -0,0 +1,69 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { jobsClient } from '@documenso/lib/jobs/client';
|
||||
import type { TClaimFlags } from '@documenso/lib/types/subscription';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateSubscriptionClaimRequestSchema,
|
||||
ZUpdateSubscriptionClaimResponseSchema,
|
||||
} from './update-subscription-claim.types';
|
||||
|
||||
export const updateSubscriptionClaimRoute = adminProcedure
|
||||
.input(ZUpdateSubscriptionClaimRequestSchema)
|
||||
.output(ZUpdateSubscriptionClaimResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id, data } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input,
|
||||
});
|
||||
|
||||
const existingClaim = await prisma.subscriptionClaim.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!existingClaim) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Subscription claim not found' });
|
||||
}
|
||||
|
||||
const newlyEnabledFlags = getNewTruthyFlags(existingClaim.flags, data.flags);
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.subscriptionClaim.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
if (Object.keys(newlyEnabledFlags).length > 0) {
|
||||
await jobsClient.triggerJob({
|
||||
name: 'internal.backport-subscription-claims',
|
||||
payload: {
|
||||
subscriptionClaimId: id,
|
||||
flags: newlyEnabledFlags,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getNewTruthyFlags(
|
||||
a: Partial<TClaimFlags>,
|
||||
b: Partial<TClaimFlags>,
|
||||
): Record<keyof TClaimFlags, true> {
|
||||
const flags: { [key in keyof TClaimFlags]?: true } = {};
|
||||
|
||||
for (const key in b) {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const typedKey = key as keyof TClaimFlags;
|
||||
|
||||
if (b[typedKey] === true && a[typedKey] !== true) {
|
||||
flags[typedKey] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return flags as Record<keyof TClaimFlags, true>;
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZCreateSubscriptionClaimRequestSchema } from './create-subscription-claim.types';
|
||||
|
||||
export const ZUpdateSubscriptionClaimRequestSchema = z.object({
|
||||
id: z.string(),
|
||||
data: ZCreateSubscriptionClaimRequestSchema,
|
||||
});
|
||||
|
||||
export const ZUpdateSubscriptionClaimResponseSchema = z.void();
|
||||
|
||||
export type TUpdateSubscriptionClaimRequest = z.infer<typeof ZUpdateSubscriptionClaimRequestSchema>;
|
||||
@ -1,36 +1,26 @@
|
||||
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
||||
import { deleteTokenById } from '@documenso/lib/server-only/public-api/delete-api-token-by-id';
|
||||
import { getApiTokenById } from '@documenso/lib/server-only/public-api/get-api-token-by-id';
|
||||
import { getApiTokens } from '@documenso/lib/server-only/public-api/get-api-tokens';
|
||||
|
||||
import { authenticatedProcedure, router } from '../trpc';
|
||||
import {
|
||||
ZCreateTokenMutationSchema,
|
||||
ZDeleteTokenByIdMutationSchema,
|
||||
ZGetApiTokenByIdQuerySchema,
|
||||
} from './schema';
|
||||
import { ZCreateTokenMutationSchema, ZDeleteTokenByIdMutationSchema } from './schema';
|
||||
|
||||
export const apiTokenRouter = router({
|
||||
getTokens: authenticatedProcedure.query(async ({ ctx }) => {
|
||||
return await getApiTokens({ userId: ctx.user.id, teamId: ctx.teamId });
|
||||
}),
|
||||
|
||||
getTokenById: authenticatedProcedure
|
||||
.input(ZGetApiTokenByIdQuerySchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { id } = input;
|
||||
|
||||
return await getApiTokenById({
|
||||
id,
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
}),
|
||||
|
||||
createToken: authenticatedProcedure
|
||||
.input(ZCreateTokenMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { tokenName, teamId, expirationDate } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
return await createApiToken({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -44,6 +34,13 @@ export const apiTokenRouter = router({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id, teamId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
return await deleteTokenById({
|
||||
id,
|
||||
teamId,
|
||||
|
||||
@ -1,13 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZGetApiTokenByIdQuerySchema = z.object({
|
||||
id: z.number().min(1),
|
||||
});
|
||||
|
||||
export type TGetApiTokenByIdQuerySchema = z.infer<typeof ZGetApiTokenByIdQuerySchema>;
|
||||
|
||||
export const ZCreateTokenMutationSchema = z.object({
|
||||
teamId: z.number().optional(),
|
||||
teamId: z.number(),
|
||||
tokenName: z.string().min(3, { message: 'The token name should be 3 characters or longer' }),
|
||||
expirationDate: z.string().nullable(),
|
||||
});
|
||||
@ -16,7 +10,7 @@ export type TCreateTokenMutationSchema = z.infer<typeof ZCreateTokenMutationSche
|
||||
|
||||
export const ZDeleteTokenByIdMutationSchema = z.object({
|
||||
id: z.number().min(1),
|
||||
teamId: z.number().optional(),
|
||||
teamId: z.number(),
|
||||
});
|
||||
|
||||
export type TDeleteTokenByIdMutationSchema = z.infer<typeof ZDeleteTokenByIdMutationSchema>;
|
||||
|
||||
@ -66,6 +66,12 @@ export const authRouter = router({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { passkeyId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
passkeyId,
|
||||
},
|
||||
});
|
||||
|
||||
await deletePasskey({
|
||||
userId: ctx.user.id,
|
||||
passkeyId,
|
||||
@ -91,6 +97,12 @@ export const authRouter = router({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { passkeyId, name } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
passkeyId,
|
||||
},
|
||||
});
|
||||
|
||||
await updatePasskey({
|
||||
userId: ctx.user.id,
|
||||
passkeyId,
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
import type { Session } from '@prisma/client';
|
||||
import type { Context } from 'hono';
|
||||
import type { Logger } from 'pino';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import type { RootApiLog } from '@documenso/lib/types/api-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { alphaid } from '@documenso/lib/universal/id';
|
||||
import { logger } from '@documenso/lib/utils/logger';
|
||||
// This is a bit nasty. Todo: Extract
|
||||
import type { HonoEnv } from '@documenso/remix/server/router';
|
||||
|
||||
type CreateTrpcContextOptions = {
|
||||
c: Context;
|
||||
c: Context<HonoEnv>;
|
||||
requestSource: 'app' | 'apiV1' | 'apiV2';
|
||||
};
|
||||
|
||||
@ -20,14 +25,22 @@ export const createTrpcContext = async ({
|
||||
|
||||
const req = c.req.raw;
|
||||
|
||||
const requestMetadata = c.get('context').requestMetadata;
|
||||
|
||||
const metadata: ApiRequestMetadata = {
|
||||
requestMetadata: extractRequestMetadata(req),
|
||||
requestMetadata,
|
||||
source: requestSource,
|
||||
auth: null,
|
||||
};
|
||||
|
||||
const rawTeamId = req.headers.get('x-team-id') || undefined;
|
||||
|
||||
const trpcLogger = logger.child({
|
||||
ipAddress: requestMetadata.ipAddress,
|
||||
userAgent: requestMetadata.userAgent,
|
||||
requestId: alphaid(),
|
||||
} satisfies RootApiLog);
|
||||
|
||||
const teamId = z.coerce
|
||||
.number()
|
||||
.optional()
|
||||
@ -36,6 +49,7 @@ export const createTrpcContext = async ({
|
||||
|
||||
if (!session || !user) {
|
||||
return {
|
||||
logger: trpcLogger,
|
||||
session: null,
|
||||
user: null,
|
||||
teamId,
|
||||
@ -45,6 +59,7 @@ export const createTrpcContext = async ({
|
||||
}
|
||||
|
||||
return {
|
||||
logger: trpcLogger,
|
||||
session,
|
||||
user,
|
||||
teamId,
|
||||
@ -66,4 +81,5 @@ export type TrpcContext = (
|
||||
teamId: number | undefined;
|
||||
req: Request;
|
||||
metadata: ApiRequestMetadata;
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
93
packages/trpc/server/document-router/download-document.ts
Normal file
93
packages/trpc/server/document-router/download-document.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { DocumentDataType } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||
import { getPresignGetUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZDownloadDocumentRequestSchema, ZDownloadDocumentResponseSchema } from './schema';
|
||||
|
||||
export const downloadDocumentRoute = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/document/{documentId}/download-beta',
|
||||
summary: 'Download document (beta)',
|
||||
description: 'Get a pre-signed download URL for the original or signed version of a document',
|
||||
tags: ['Document'],
|
||||
},
|
||||
})
|
||||
.input(ZDownloadDocumentRequestSchema)
|
||||
.output(ZDownloadDocumentResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { documentId, version } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
version,
|
||||
},
|
||||
});
|
||||
|
||||
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document downloads are only available when S3 storage is configured.',
|
||||
});
|
||||
}
|
||||
|
||||
const document = await getDocumentById({
|
||||
documentId,
|
||||
userId: user.id,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!document.documentData) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document data not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.documentData.type !== DocumentDataType.S3_PATH) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document is not stored in S3 and cannot be downloaded via URL.',
|
||||
});
|
||||
}
|
||||
|
||||
if (version === 'signed' && !isDocumentCompleted(document.status)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document is not completed yet.',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const documentData =
|
||||
version === 'original'
|
||||
? document.documentData.initialData || document.documentData.data
|
||||
: document.documentData.data;
|
||||
|
||||
const { url } = await getPresignGetUrl(documentData);
|
||||
|
||||
const baseTitle = document.title.replace(/\.pdf$/, '');
|
||||
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
|
||||
const filename = `${baseTitle}${suffix}`;
|
||||
|
||||
return {
|
||||
downloadUrl: url,
|
||||
filename,
|
||||
contentType: 'application/pdf',
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.logger.error({
|
||||
error,
|
||||
message: 'Failed to generate download URL',
|
||||
documentId,
|
||||
version,
|
||||
});
|
||||
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Failed to generate download URL',
|
||||
});
|
||||
}
|
||||
});
|
||||
105
packages/trpc/server/document-router/find-inbox.ts
Normal file
105
packages/trpc/server/document-router/find-inbox.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import type { Document, Prisma } from '@prisma/client';
|
||||
import { DocumentStatus, RecipientRole } from '@prisma/client';
|
||||
|
||||
import type { FindResultResponse } from '@documenso/lib/types/search-params';
|
||||
import { maskRecipientTokensForDocument } from '@documenso/lib/utils/mask-recipient-tokens-for-document';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZFindInboxRequestSchema, ZFindInboxResponseSchema } from './find-inbox.types';
|
||||
|
||||
export const findInboxRoute = authenticatedProcedure
|
||||
.input(ZFindInboxRequestSchema)
|
||||
.output(ZFindInboxResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { page, perPage } = input;
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
return await findInbox({
|
||||
userId,
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
});
|
||||
|
||||
export type FindInboxOptions = {
|
||||
userId: number;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
orderBy?: {
|
||||
column: keyof Omit<Document, 'document'>;
|
||||
direction: 'asc' | 'desc';
|
||||
};
|
||||
};
|
||||
|
||||
export const findInbox = async ({ userId, page = 1, perPage = 10, orderBy }: FindInboxOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const orderByColumn = orderBy?.column ?? 'createdAt';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
|
||||
const whereClause: Prisma.DocumentWhereInput = {
|
||||
status: {
|
||||
not: DocumentStatus.DRAFT,
|
||||
},
|
||||
deletedAt: null,
|
||||
recipients: {
|
||||
some: {
|
||||
email: user.email,
|
||||
role: {
|
||||
not: RecipientRole.CC,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.document.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
[orderByColumn]: orderByDirection,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.document.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
const maskedData = data.map((document) =>
|
||||
maskRecipientTokensForDocument({
|
||||
document,
|
||||
user,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
data: maskedData,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
13
packages/trpc/server/document-router/find-inbox.types.ts
Normal file
13
packages/trpc/server/document-router/find-inbox.types.ts
Normal file
@ -0,0 +1,13 @@
|
||||
// import type { OpenApiMeta } from 'trpc-to-openapi';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { ZDocumentManySchema } from '@documenso/lib/types/document';
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
|
||||
export const ZFindInboxRequestSchema = ZFindSearchParamsSchema;
|
||||
|
||||
export const ZFindInboxResponseSchema = ZFindResultResponse.extend({
|
||||
data: ZDocumentManySchema.array(),
|
||||
});
|
||||
|
||||
export type TFindInboxResponse = z.infer<typeof ZFindInboxResponseSchema>;
|
||||
35
packages/trpc/server/document-router/get-inbox-count.ts
Normal file
35
packages/trpc/server/document-router/get-inbox-count.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { DocumentStatus, RecipientRole } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZGetInboxCountRequestSchema, ZGetInboxCountResponseSchema } from './get-inbox-count.types';
|
||||
|
||||
export const getInboxCountRoute = authenticatedProcedure
|
||||
.input(ZGetInboxCountRequestSchema)
|
||||
.output(ZGetInboxCountResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { readStatus } = input ?? {};
|
||||
|
||||
const userEmail = ctx.user.email;
|
||||
|
||||
const count = await prisma.recipient.count({
|
||||
where: {
|
||||
email: userEmail,
|
||||
readStatus,
|
||||
role: {
|
||||
not: RecipientRole.CC,
|
||||
},
|
||||
document: {
|
||||
status: {
|
||||
not: DocumentStatus.DRAFT,
|
||||
},
|
||||
deletedAt: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
count,
|
||||
};
|
||||
});
|
||||
@ -0,0 +1,15 @@
|
||||
// import type { OpenApiMeta } from 'trpc-to-openapi';
|
||||
import { ReadStatus } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZGetInboxCountRequestSchema = z
|
||||
.object({
|
||||
readStatus: z.nativeEnum(ReadStatus).optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
export const ZGetInboxCountResponseSchema = z.object({
|
||||
count: z.number(),
|
||||
});
|
||||
|
||||
export type TGetInboxCountResponse = z.infer<typeof ZGetInboxCountResponseSchema>;
|
||||
@ -1,5 +1,4 @@
|
||||
import { DocumentDataType } from '@prisma/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
@ -19,16 +18,17 @@ import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document
|
||||
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
||||
import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats';
|
||||
import { getStats } from '@documenso/lib/server-only/document/get-stats';
|
||||
import { moveDocumentToTeam } from '@documenso/lib/server-only/document/move-document-to-team';
|
||||
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
||||
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
|
||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
|
||||
import { authenticatedProcedure, procedure, router } from '../trpc';
|
||||
import { downloadDocumentRoute } from './download-document';
|
||||
import { findInboxRoute } from './find-inbox';
|
||||
import { getInboxCountRoute } from './get-inbox-count';
|
||||
import {
|
||||
ZCreateDocumentRequestSchema,
|
||||
ZCreateDocumentV2RequestSchema,
|
||||
@ -50,20 +50,21 @@ import {
|
||||
ZGetDocumentByTokenQuerySchema,
|
||||
ZGetDocumentWithDetailsByIdRequestSchema,
|
||||
ZGetDocumentWithDetailsByIdResponseSchema,
|
||||
ZMoveDocumentToTeamResponseSchema,
|
||||
ZMoveDocumentToTeamSchema,
|
||||
ZResendDocumentMutationSchema,
|
||||
ZSearchDocumentsMutationSchema,
|
||||
ZSetSigningOrderForDocumentMutationSchema,
|
||||
ZSuccessResponseSchema,
|
||||
} from './schema';
|
||||
import { updateDocumentRoute } from './update-document';
|
||||
import {
|
||||
ZUpdateDocumentRequestSchema,
|
||||
ZUpdateDocumentResponseSchema,
|
||||
} from './update-document.types';
|
||||
|
||||
export const documentRouter = router({
|
||||
inbox: {
|
||||
find: findInboxRoute,
|
||||
getCount: getInboxCountRoute,
|
||||
},
|
||||
updateDocument: updateDocumentRoute,
|
||||
downloadDocument: downloadDocumentRoute,
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@ -73,6 +74,12 @@ export const documentRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { documentId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
return await getDocumentById({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -112,8 +119,17 @@ export const documentRouter = router({
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { user, teamId } = ctx;
|
||||
|
||||
const { query, templateId, page, perPage, orderByDirection, orderByColumn, source, status } =
|
||||
input;
|
||||
const {
|
||||
query,
|
||||
templateId,
|
||||
page,
|
||||
perPage,
|
||||
orderByDirection,
|
||||
orderByColumn,
|
||||
source,
|
||||
status,
|
||||
folderId,
|
||||
} = input;
|
||||
|
||||
const documents = await findDocuments({
|
||||
userId: user.id,
|
||||
@ -124,6 +140,7 @@ export const documentRouter = router({
|
||||
status,
|
||||
page,
|
||||
perPage,
|
||||
folderId,
|
||||
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
|
||||
});
|
||||
|
||||
@ -152,12 +169,14 @@ export const documentRouter = router({
|
||||
status,
|
||||
period,
|
||||
senderIds,
|
||||
folderId,
|
||||
} = input;
|
||||
|
||||
const getStatOptions: GetStatsInput = {
|
||||
user,
|
||||
period,
|
||||
search: query,
|
||||
folderId,
|
||||
};
|
||||
|
||||
if (teamId) {
|
||||
@ -167,7 +186,7 @@ export const documentRouter = router({
|
||||
teamId: team.id,
|
||||
teamEmail: team.teamEmail?.email,
|
||||
senderIds,
|
||||
currentTeamMemberRole: team.currentTeamMember?.role,
|
||||
currentTeamMemberRole: team.currentTeamRole,
|
||||
currentUserEmail: user.email,
|
||||
userId: user.id,
|
||||
};
|
||||
@ -186,6 +205,7 @@ export const documentRouter = router({
|
||||
status,
|
||||
period,
|
||||
senderIds,
|
||||
folderId,
|
||||
orderBy: orderByColumn
|
||||
? { column: orderByColumn, direction: orderByDirection }
|
||||
: undefined,
|
||||
@ -217,12 +237,20 @@ export const documentRouter = router({
|
||||
.output(ZGetDocumentWithDetailsByIdResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { documentId } = input;
|
||||
const { documentId, folderId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
folderId,
|
||||
},
|
||||
});
|
||||
|
||||
return await getDocumentWithDetailsById({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
documentId,
|
||||
folderId,
|
||||
});
|
||||
}),
|
||||
|
||||
@ -246,7 +274,7 @@ export const documentRouter = router({
|
||||
.input(ZCreateDocumentV2RequestSchema)
|
||||
.output(ZCreateDocumentV2ResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx;
|
||||
const { teamId, user } = ctx;
|
||||
|
||||
const {
|
||||
title,
|
||||
@ -258,7 +286,7 @@ export const documentRouter = router({
|
||||
meta,
|
||||
} = input;
|
||||
|
||||
const { remaining } = await getServerLimits({ email: ctx.user.email, teamId });
|
||||
const { remaining } = await getServerLimits({ userId: user.id, teamId });
|
||||
|
||||
if (remaining.documents <= 0) {
|
||||
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
|
||||
@ -295,6 +323,7 @@ export const documentRouter = router({
|
||||
|
||||
return {
|
||||
document: createdDocument,
|
||||
folder: createdDocument.folder, // Todo: Remove this prior to api-v2 release.
|
||||
uploadUrl: url,
|
||||
};
|
||||
}),
|
||||
@ -315,10 +344,16 @@ export const documentRouter = router({
|
||||
// })
|
||||
.input(ZCreateDocumentRequestSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx;
|
||||
const { title, documentDataId, timezone } = input;
|
||||
const { user, teamId } = ctx;
|
||||
const { title, documentDataId, timezone, folderId } = input;
|
||||
|
||||
const { remaining } = await getServerLimits({ email: ctx.user.email, teamId });
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
folderId,
|
||||
},
|
||||
});
|
||||
|
||||
const { remaining } = await getServerLimits({ userId: user.id, teamId });
|
||||
|
||||
if (remaining.documents <= 0) {
|
||||
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
|
||||
@ -328,58 +363,14 @@ export const documentRouter = router({
|
||||
}
|
||||
|
||||
return await createDocument({
|
||||
userId: ctx.user.id,
|
||||
userId: user.id,
|
||||
teamId,
|
||||
title,
|
||||
documentDataId,
|
||||
normalizePdf: true,
|
||||
timezone,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
}),
|
||||
|
||||
updateDocument: updateDocumentRoute,
|
||||
|
||||
/**
|
||||
* @deprecated Delete this after updateDocument endpoint is deployed
|
||||
*/
|
||||
setSettingsForDocument: authenticatedProcedure
|
||||
.input(ZUpdateDocumentRequestSchema)
|
||||
.output(ZUpdateDocumentResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx;
|
||||
const { documentId, data, meta = {} } = input;
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
if (Object.values(meta).length > 0) {
|
||||
await upsertDocumentMeta({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
documentId,
|
||||
subject: meta.subject,
|
||||
message: meta.message,
|
||||
timezone: meta.timezone,
|
||||
dateFormat: meta.dateFormat,
|
||||
language: meta.language,
|
||||
typedSignatureEnabled: meta.typedSignatureEnabled,
|
||||
uploadSignatureEnabled: meta.uploadSignatureEnabled,
|
||||
drawSignatureEnabled: meta.drawSignatureEnabled,
|
||||
redirectUrl: meta.redirectUrl,
|
||||
distributionMethod: meta.distributionMethod,
|
||||
signingOrder: meta.signingOrder,
|
||||
allowDictateNextSigner: meta.allowDictateNextSigner,
|
||||
emailSettings: meta.emailSettings,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
return await updateDocument({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
data,
|
||||
userTimezone: timezone,
|
||||
requestMetadata: ctx.metadata,
|
||||
folderId,
|
||||
});
|
||||
}),
|
||||
|
||||
@ -401,6 +392,12 @@ export const documentRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { documentId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
await deleteDocument({
|
||||
@ -413,33 +410,6 @@ export const documentRouter = router({
|
||||
return ZGenericSuccessResponse;
|
||||
}),
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
moveDocumentToTeam: authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/document/move',
|
||||
summary: 'Move document',
|
||||
description: 'Move a document from your personal account to a team',
|
||||
tags: ['Document'],
|
||||
},
|
||||
})
|
||||
.input(ZMoveDocumentToTeamSchema)
|
||||
.output(ZMoveDocumentToTeamResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { documentId, teamId } = input;
|
||||
const userId = ctx.user.id;
|
||||
|
||||
return await moveDocumentToTeam({
|
||||
documentId,
|
||||
teamId,
|
||||
userId,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
}),
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
@ -451,6 +421,13 @@ export const documentRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { documentId, signingOrder } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
signingOrder,
|
||||
},
|
||||
});
|
||||
|
||||
return await upsertDocumentMeta({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -482,6 +459,12 @@ export const documentRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { documentId, meta = {} } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
if (Object.values(meta).length > 0) {
|
||||
await upsertDocumentMeta({
|
||||
userId: ctx.user.id,
|
||||
@ -495,6 +478,8 @@ export const documentRouter = router({
|
||||
distributionMethod: meta.distributionMethod,
|
||||
emailSettings: meta.emailSettings,
|
||||
language: meta.language,
|
||||
emailId: meta.emailId,
|
||||
emailReplyTo: meta.emailReplyTo,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
}
|
||||
@ -529,6 +514,13 @@ export const documentRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { documentId, recipients } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
recipients,
|
||||
},
|
||||
});
|
||||
|
||||
await resendDocument({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -558,6 +550,12 @@ export const documentRouter = router({
|
||||
const { teamId, user } = ctx;
|
||||
const { documentId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
return await duplicateDocument({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
@ -599,6 +597,12 @@ export const documentRouter = router({
|
||||
orderByDirection,
|
||||
} = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
return await findDocumentAuditLogs({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -620,6 +624,12 @@ export const documentRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { documentId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
const document = await getDocumentById({
|
||||
documentId,
|
||||
userId: ctx.user.id,
|
||||
@ -627,8 +637,7 @@ export const documentRouter = router({
|
||||
}).catch(() => null);
|
||||
|
||||
if (!document || (teamId && document.teamId !== teamId)) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have access to this document.',
|
||||
});
|
||||
}
|
||||
@ -652,6 +661,12 @@ export const documentRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { documentId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
const document = await getDocumentById({
|
||||
documentId,
|
||||
userId: ctx.user.id,
|
||||
|
||||
@ -130,6 +130,7 @@ export const ZFindDocumentsRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
.nativeEnum(DocumentStatus)
|
||||
.describe('Filter documents by the current status')
|
||||
.optional(),
|
||||
folderId: z.string().describe('Filter documents by folder ID').optional(),
|
||||
orderByColumn: z.enum(['createdAt']).optional(),
|
||||
orderByDirection: z.enum(['asc', 'desc']).describe('').default('desc'),
|
||||
});
|
||||
@ -144,6 +145,7 @@ export const ZFindDocumentsInternalRequestSchema = ZFindDocumentsRequestSchema.e
|
||||
period: z.enum(['7d', '14d', '30d']).optional(),
|
||||
senderIds: z.array(z.number()).optional(),
|
||||
status: z.nativeEnum(ExtendedDocumentStatus).optional(),
|
||||
folderId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ZFindDocumentsInternalResponseSchema = ZFindResultResponse.extend({
|
||||
@ -188,6 +190,7 @@ export type TGetDocumentByTokenQuerySchema = z.infer<typeof ZGetDocumentByTokenQ
|
||||
|
||||
export const ZGetDocumentWithDetailsByIdRequestSchema = z.object({
|
||||
documentId: z.number(),
|
||||
folderId: z.string().describe('Filter documents by folder ID').optional(),
|
||||
});
|
||||
|
||||
export const ZGetDocumentWithDetailsByIdResponseSchema = ZDocumentSchema;
|
||||
@ -196,14 +199,15 @@ export const ZCreateDocumentRequestSchema = z.object({
|
||||
title: ZDocumentTitleSchema,
|
||||
documentDataId: z.string().min(1),
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional(),
|
||||
folderId: z.string().describe('The ID of the folder to create the document in').optional(),
|
||||
});
|
||||
|
||||
export const ZCreateDocumentV2RequestSchema = z.object({
|
||||
title: ZDocumentTitleSchema,
|
||||
externalId: ZDocumentExternalIdSchema.optional(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||
formValues: ZDocumentFormValuesSchema.optional(),
|
||||
recipients: z
|
||||
.array(
|
||||
@ -290,6 +294,8 @@ export const ZDistributeDocumentRequestSchema = z.object({
|
||||
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: z.string().email().nullish(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
@ -341,9 +347,21 @@ export const ZDownloadCertificateMutationSchema = z.object({
|
||||
documentId: z.number(),
|
||||
});
|
||||
|
||||
export const ZMoveDocumentToTeamSchema = z.object({
|
||||
documentId: z.number().describe('The ID of the document to move to a team.'),
|
||||
teamId: z.number().describe('The ID of the team to move the document to.'),
|
||||
export const ZDownloadDocumentRequestSchema = z.object({
|
||||
documentId: z.number().describe('The ID of the document to download.'),
|
||||
version: z
|
||||
.enum(['original', 'signed'])
|
||||
.describe(
|
||||
'The version of the document to download. "signed" returns the completed document with signatures, "original" returns the original uploaded document.',
|
||||
)
|
||||
.default('signed'),
|
||||
});
|
||||
|
||||
export const ZMoveDocumentToTeamResponseSchema = ZDocumentLiteSchema;
|
||||
export const ZDownloadDocumentResponseSchema = z.object({
|
||||
downloadUrl: z.string().describe('Pre-signed URL for downloading the PDF file'),
|
||||
filename: z.string().describe('The filename of the PDF file'),
|
||||
contentType: z.string().describe('MIME type of the file'),
|
||||
});
|
||||
|
||||
export type TDownloadDocumentRequest = z.infer<typeof ZDownloadDocumentRequestSchema>;
|
||||
export type TDownloadDocumentResponse = z.infer<typeof ZDownloadDocumentResponseSchema>;
|
||||
|
||||
@ -19,6 +19,12 @@ export const updateDocumentRoute = authenticatedProcedure
|
||||
const { teamId } = ctx;
|
||||
const { documentId, data, meta = {} } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
if (Object.values(meta).length > 0) {
|
||||
@ -38,6 +44,8 @@ export const updateDocumentRoute = authenticatedProcedure
|
||||
distributionMethod: meta.distributionMethod,
|
||||
signingOrder: meta.signingOrder,
|
||||
allowDictateNextSigner: meta.allowDictateNextSigner,
|
||||
emailId: meta.emailId,
|
||||
emailReplyTo: meta.emailReplyTo,
|
||||
emailSettings: meta.emailSettings,
|
||||
reminderInterval: meta.reminderInterval,
|
||||
requestMetadata: ctx.metadata,
|
||||
|
||||
@ -42,8 +42,9 @@ export const ZUpdateDocumentRequestSchema = z.object({
|
||||
title: ZDocumentTitleSchema.optional(),
|
||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||
useLegacyFieldInsertion: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
meta: z
|
||||
@ -60,6 +61,8 @@ export const ZUpdateDocumentRequestSchema = z.object({
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||
emailId: z.string().nullish(),
|
||||
emailReplyTo: z.string().email().nullish(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
reminderInterval: z.nativeEnum(DocumentReminderInterval).optional(),
|
||||
})
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { router } from '../trpc';
|
||||
import { applyMultiSignSignatureRoute } from './apply-multi-sign-signature';
|
||||
import { createEmbeddingDocumentRoute } from './create-embedding-document';
|
||||
import { createEmbeddingPresignTokenRoute } from './create-embedding-presign-token';
|
||||
import { createEmbeddingTemplateRoute } from './create-embedding-template';
|
||||
import { getEmbeddingDocumentRoute } from './get-embedding-document';
|
||||
import { getMultiSignDocumentRoute } from './get-multi-sign-document';
|
||||
import { updateEmbeddingDocumentRoute } from './update-embedding-document';
|
||||
import { updateEmbeddingTemplateRoute } from './update-embedding-template';
|
||||
import { verifyEmbeddingPresignTokenRoute } from './verify-embedding-presign-token';
|
||||
|
||||
export const embeddingPresignRouter = router({
|
||||
@ -10,5 +13,8 @@ export const embeddingPresignRouter = router({
|
||||
verifyEmbeddingPresignToken: verifyEmbeddingPresignTokenRoute,
|
||||
createEmbeddingDocument: createEmbeddingDocumentRoute,
|
||||
createEmbeddingTemplate: createEmbeddingTemplateRoute,
|
||||
getEmbeddingDocument: getEmbeddingDocumentRoute,
|
||||
updateEmbeddingDocument: updateEmbeddingDocumentRoute,
|
||||
updateEmbeddingTemplate: updateEmbeddingTemplateRoute,
|
||||
applyMultiSignSignature: applyMultiSignSignatureRoute,
|
||||
getMultiSignDocument: getMultiSignDocumentRoute,
|
||||
});
|
||||
|
||||
@ -0,0 +1,102 @@
|
||||
import { FieldType, ReadStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { signFieldWithToken } from '@documenso/lib/server-only/field/sign-field-with-token';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { procedure } from '../trpc';
|
||||
import {
|
||||
ZApplyMultiSignSignatureRequestSchema,
|
||||
ZApplyMultiSignSignatureResponseSchema,
|
||||
} from './apply-multi-sign-signature.types';
|
||||
|
||||
export const applyMultiSignSignatureRoute = procedure
|
||||
.input(ZApplyMultiSignSignatureRequestSchema)
|
||||
.output(ZApplyMultiSignSignatureResponseSchema)
|
||||
.mutation(async ({ input, ctx: { metadata } }) => {
|
||||
try {
|
||||
const { tokens, signature, isBase64 } = input;
|
||||
|
||||
// Get all documents and recipients for the tokens
|
||||
const envelopes = await Promise.all(
|
||||
tokens.map(async (token) => {
|
||||
const document = await getDocumentByToken({ token });
|
||||
const recipient = await getRecipientByToken({ token });
|
||||
|
||||
return { document, recipient };
|
||||
}),
|
||||
);
|
||||
|
||||
// Check if all documents have been viewed
|
||||
const hasUnviewedDocuments = envelopes.some(
|
||||
(envelope) => envelope.recipient.readStatus !== ReadStatus.OPENED,
|
||||
);
|
||||
|
||||
if (hasUnviewedDocuments) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'All documents must be viewed before signing',
|
||||
});
|
||||
}
|
||||
|
||||
// If we require action auth we should abort here for now
|
||||
for (const envelope of envelopes) {
|
||||
const derivedRecipientActionAuth = extractDocumentAuthMethods({
|
||||
documentAuth: envelope.document.authOptions,
|
||||
recipientAuth: envelope.recipient.authOptions,
|
||||
});
|
||||
|
||||
if (
|
||||
derivedRecipientActionAuth.recipientAccessAuthRequired ||
|
||||
derivedRecipientActionAuth.recipientActionAuthRequired
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Documents that require additional authentication cannot be multi signed at the moment',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sign all signature fields for each document
|
||||
await Promise.all(
|
||||
envelopes.map(async (envelope) => {
|
||||
if (envelope.recipient.signingStatus === SigningStatus.REJECTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const signatureFields = await prisma.field.findMany({
|
||||
where: {
|
||||
documentId: envelope.document.id,
|
||||
recipientId: envelope.recipient.id,
|
||||
type: FieldType.SIGNATURE,
|
||||
inserted: false,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
signatureFields.map(async (field) =>
|
||||
signFieldWithToken({
|
||||
token: envelope.recipient.token,
|
||||
fieldId: field.id,
|
||||
value: signature,
|
||||
isBase64,
|
||||
requestMetadata: metadata.requestMetadata,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Failed to apply multi-sign signature',
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZApplyMultiSignSignatureRequestSchema = z.object({
|
||||
tokens: z.array(z.string()).min(1, { message: 'At least one token is required' }),
|
||||
signature: z.string().min(1, { message: 'Signature is required' }),
|
||||
isBase64: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export const ZApplyMultiSignSignatureResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
});
|
||||
|
||||
export type TApplyMultiSignSignatureRequestSchema = z.infer<
|
||||
typeof ZApplyMultiSignSignatureRequestSchema
|
||||
>;
|
||||
export type TApplyMultiSignSignatureResponseSchema = z.infer<
|
||||
typeof ZApplyMultiSignSignatureResponseSchema
|
||||
>;
|
||||
@ -1,9 +1,7 @@
|
||||
import { isCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan';
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/create-embedding-presign-token';
|
||||
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
|
||||
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
|
||||
|
||||
import { procedure } from '../trpc';
|
||||
@ -42,13 +40,11 @@ export const createEmbeddingPresignTokenRoute = procedure
|
||||
});
|
||||
}
|
||||
|
||||
const [hasCommunityPlan, hasPlatformPlan, hasEnterprisePlan] = await Promise.all([
|
||||
isCommunityPlan({ userId: token.userId, teamId: token.teamId ?? undefined }),
|
||||
isDocumentPlatform({ userId: token.userId, teamId: token.teamId }),
|
||||
isUserEnterprise({ userId: token.userId, teamId: token.teamId ?? undefined }),
|
||||
]);
|
||||
const organisationClaim = await getOrganisationClaimByTeamId({
|
||||
teamId: token.teamId,
|
||||
});
|
||||
|
||||
if (!hasCommunityPlan && !hasPlatformPlan && !hasEnterprisePlan) {
|
||||
if (!organisationClaim.flags.embedAuthoring) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to create embedding presign tokens',
|
||||
});
|
||||
|
||||
@ -33,7 +33,9 @@ export const createEmbeddingTemplateRoute = procedure
|
||||
// First create the template
|
||||
const template = await createTemplate({
|
||||
userId: apiToken.userId,
|
||||
title,
|
||||
data: {
|
||||
title,
|
||||
},
|
||||
templateDocumentDataId: documentDataId,
|
||||
teamId: apiToken.teamId ?? undefined,
|
||||
});
|
||||
@ -77,16 +79,31 @@ export const createEmbeddingTemplateRoute = procedure
|
||||
|
||||
// Update the template meta if needed
|
||||
if (meta) {
|
||||
const upsertMetaData = {
|
||||
subject: meta.subject,
|
||||
message: meta.message,
|
||||
timezone: meta.timezone,
|
||||
dateFormat: meta.dateFormat,
|
||||
distributionMethod: meta.distributionMethod,
|
||||
signingOrder: meta.signingOrder,
|
||||
redirectUrl: meta.redirectUrl,
|
||||
language: meta.language,
|
||||
typedSignatureEnabled: meta.typedSignatureEnabled,
|
||||
drawSignatureEnabled: meta.drawSignatureEnabled,
|
||||
uploadSignatureEnabled: meta.uploadSignatureEnabled,
|
||||
emailSettings: meta.emailSettings,
|
||||
};
|
||||
|
||||
await prisma.templateMeta.upsert({
|
||||
where: {
|
||||
templateId: template.id,
|
||||
},
|
||||
create: {
|
||||
templateId: template.id,
|
||||
...meta,
|
||||
...upsertMetaData,
|
||||
},
|
||||
update: {
|
||||
...meta,
|
||||
...upsertMetaData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { procedure } from '../trpc';
|
||||
import {
|
||||
ZGetEmbeddingDocumentRequestSchema,
|
||||
ZGetEmbeddingDocumentResponseSchema,
|
||||
} from './get-embedding-document.types';
|
||||
|
||||
export const getEmbeddingDocumentRoute = procedure
|
||||
.input(ZGetEmbeddingDocumentRequestSchema)
|
||||
.output(ZGetEmbeddingDocumentResponseSchema)
|
||||
.query(async ({ input, ctx: { req } }) => {
|
||||
try {
|
||||
const authorizationHeader = req.headers.get('authorization');
|
||||
|
||||
const [presignToken] = (authorizationHeader || '')
|
||||
.split('Bearer ')
|
||||
.filter((s) => s.length > 0);
|
||||
|
||||
if (!presignToken) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'No presign token provided',
|
||||
});
|
||||
}
|
||||
|
||||
const apiToken = await verifyEmbeddingPresignToken({ token: presignToken });
|
||||
|
||||
const { documentId } = input;
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
userId: apiToken.userId,
|
||||
...(apiToken.teamId ? { teamId: apiToken.teamId } : {}),
|
||||
},
|
||||
include: {
|
||||
documentData: true,
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
document,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Failed to get document',
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -1,34 +0,0 @@
|
||||
import { DocumentDataType, type Field, type Recipient } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZGetEmbeddingDocumentRequestSchema = z.object({
|
||||
documentId: z.number(),
|
||||
});
|
||||
|
||||
export const ZGetEmbeddingDocumentResponseSchema = z.object({
|
||||
document: z
|
||||
.object({
|
||||
id: z.number(),
|
||||
title: z.string(),
|
||||
status: z.string(),
|
||||
documentDataId: z.string(),
|
||||
userId: z.number(),
|
||||
teamId: z.number().nullable(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
documentData: z.object({
|
||||
id: z.string(),
|
||||
type: z.nativeEnum(DocumentDataType),
|
||||
data: z.string(),
|
||||
initialData: z.string(),
|
||||
}),
|
||||
recipients: z.array(z.custom<Recipient>()),
|
||||
fields: z.array(z.custom<Field>()),
|
||||
})
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export type TGetEmbeddingDocumentRequestSchema = z.infer<typeof ZGetEmbeddingDocumentRequestSchema>;
|
||||
export type TGetEmbeddingDocumentResponseSchema = z.infer<
|
||||
typeof ZGetEmbeddingDocumentResponseSchema
|
||||
>;
|
||||
@ -0,0 +1,62 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
||||
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
|
||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
|
||||
import { procedure } from '../trpc';
|
||||
import {
|
||||
ZGetMultiSignDocumentRequestSchema,
|
||||
ZGetMultiSignDocumentResponseSchema,
|
||||
} from './get-multi-sign-document.types';
|
||||
|
||||
export const getMultiSignDocumentRoute = procedure
|
||||
.input(ZGetMultiSignDocumentRequestSchema)
|
||||
.output(ZGetMultiSignDocumentResponseSchema)
|
||||
.query(async ({ input, ctx: { metadata } }) => {
|
||||
try {
|
||||
const { token } = input;
|
||||
|
||||
const [document, fields, recipient] = await Promise.all([
|
||||
getDocumentAndSenderByToken({
|
||||
token,
|
||||
requireAccessAuth: false,
|
||||
}).catch(() => null),
|
||||
getFieldsForToken({ token }),
|
||||
getRecipientByToken({ token }).catch(() => null),
|
||||
getCompletedFieldsForToken({ token }).catch(() => []),
|
||||
]);
|
||||
|
||||
if (!document || !recipient) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document or recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
await viewedDocument({
|
||||
token,
|
||||
requestMetadata: metadata.requestMetadata,
|
||||
});
|
||||
|
||||
// Transform fields to match our schema
|
||||
const transformedFields = fields.map((field) => ({
|
||||
...field,
|
||||
recipient,
|
||||
}));
|
||||
|
||||
return {
|
||||
...document,
|
||||
folder: null,
|
||||
fields: transformedFields,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Failed to get document details',
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,50 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZDocumentLiteSchema } from '@documenso/lib/types/document';
|
||||
import { ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
||||
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
import DocumentMetaSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
|
||||
import FieldSchema from '@documenso/prisma/generated/zod/modelSchema/FieldSchema';
|
||||
import SignatureSchema from '@documenso/prisma/generated/zod/modelSchema/SignatureSchema';
|
||||
|
||||
export const ZGetMultiSignDocumentRequestSchema = z.object({
|
||||
token: z.string().min(1, { message: 'Token is required' }),
|
||||
});
|
||||
|
||||
export const ZGetMultiSignDocumentResponseSchema = ZDocumentLiteSchema.extend({
|
||||
documentData: DocumentDataSchema.pick({
|
||||
type: true,
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
}),
|
||||
documentMeta: DocumentMetaSchema.pick({
|
||||
signingOrder: true,
|
||||
distributionMethod: true,
|
||||
id: true,
|
||||
subject: true,
|
||||
message: true,
|
||||
timezone: true,
|
||||
password: true,
|
||||
dateFormat: true,
|
||||
documentId: true,
|
||||
redirectUrl: true,
|
||||
typedSignatureEnabled: true,
|
||||
uploadSignatureEnabled: true,
|
||||
drawSignatureEnabled: true,
|
||||
allowDictateNextSigner: true,
|
||||
language: true,
|
||||
emailSettings: true,
|
||||
}).nullable(),
|
||||
fields: z.array(
|
||||
FieldSchema.extend({
|
||||
recipient: ZRecipientLiteSchema,
|
||||
signature: SignatureSchema.nullable(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type TGetMultiSignDocumentRequestSchema = z.infer<typeof ZGetMultiSignDocumentRequestSchema>;
|
||||
export type TGetMultiSignDocumentResponseSchema = z.infer<
|
||||
typeof ZGetMultiSignDocumentResponseSchema
|
||||
>;
|
||||
@ -0,0 +1,124 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
|
||||
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
|
||||
import { procedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateEmbeddingDocumentRequestSchema,
|
||||
ZUpdateEmbeddingDocumentResponseSchema,
|
||||
} from './update-embedding-document.types';
|
||||
|
||||
export const updateEmbeddingDocumentRoute = procedure
|
||||
.input(ZUpdateEmbeddingDocumentRequestSchema)
|
||||
.output(ZUpdateEmbeddingDocumentResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId: input.documentId,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const authorizationHeader = ctx.req.headers.get('authorization');
|
||||
|
||||
const [presignToken] = (authorizationHeader || '')
|
||||
.split('Bearer ')
|
||||
.filter((s) => s.length > 0);
|
||||
|
||||
if (!presignToken) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'No presign token provided',
|
||||
});
|
||||
}
|
||||
|
||||
const apiToken = await verifyEmbeddingPresignToken({ token: presignToken });
|
||||
|
||||
const { documentId, title, externalId, recipients, meta } = input;
|
||||
|
||||
if (meta && Object.values(meta).length > 0) {
|
||||
await upsertDocumentMeta({
|
||||
documentId: documentId,
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId ?? undefined,
|
||||
...meta,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
await updateDocument({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId ?? undefined,
|
||||
documentId: documentId,
|
||||
data: {
|
||||
title,
|
||||
externalId,
|
||||
},
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
const recipientsWithClientId = recipients.map((recipient) => ({
|
||||
...recipient,
|
||||
clientId: nanoid(),
|
||||
}));
|
||||
|
||||
const { recipients: updatedRecipients } = await setDocumentRecipients({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId ?? undefined,
|
||||
documentId: documentId,
|
||||
recipients: recipientsWithClientId.map((recipient) => ({
|
||||
id: recipient.id,
|
||||
clientId: recipient.clientId,
|
||||
email: recipient.email,
|
||||
name: recipient.name ?? '',
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
})),
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
const fields = recipientsWithClientId.flatMap((recipient) => {
|
||||
const recipientId = updatedRecipients.find((r) => r.clientId === recipient.clientId)?.id;
|
||||
|
||||
if (!recipientId) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
return (recipient.fields ?? []).map((field) => ({
|
||||
...field,
|
||||
recipientId,
|
||||
// !: Temp property to be removed once we don't link based on signer email
|
||||
signerEmail: recipient.email,
|
||||
}));
|
||||
});
|
||||
|
||||
await setFieldsForDocument({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId ?? undefined,
|
||||
documentId,
|
||||
fields: fields.map((field) => ({
|
||||
...field,
|
||||
pageWidth: field.width,
|
||||
pageHeight: field.height,
|
||||
})),
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
documentId,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Failed to update document',
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,87 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import {
|
||||
ZFieldHeightSchema,
|
||||
ZFieldPageNumberSchema,
|
||||
ZFieldPageXSchema,
|
||||
ZFieldPageYSchema,
|
||||
ZFieldWidthSchema,
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { DocumentSigningOrder, RecipientRole } from '@documenso/prisma/generated/types';
|
||||
|
||||
import {
|
||||
ZDocumentExternalIdSchema,
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaDistributionMethodSchema,
|
||||
ZDocumentMetaDrawSignatureEnabledSchema,
|
||||
ZDocumentMetaLanguageSchema,
|
||||
ZDocumentMetaMessageSchema,
|
||||
ZDocumentMetaRedirectUrlSchema,
|
||||
ZDocumentMetaSubjectSchema,
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
ZDocumentMetaTypedSignatureEnabledSchema,
|
||||
ZDocumentMetaUploadSignatureEnabledSchema,
|
||||
ZDocumentTitleSchema,
|
||||
} from '../document-router/schema';
|
||||
|
||||
export const ZUpdateEmbeddingDocumentRequestSchema = z.object({
|
||||
documentId: z.number(),
|
||||
title: ZDocumentTitleSchema,
|
||||
externalId: ZDocumentExternalIdSchema.optional(),
|
||||
recipients: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
email: z.string().toLowerCase().email().min(1),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
fields: ZFieldAndMetaSchema.and(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
pageNumber: ZFieldPageNumberSchema,
|
||||
pageX: ZFieldPageXSchema,
|
||||
pageY: ZFieldPageYSchema,
|
||||
width: ZFieldWidthSchema,
|
||||
height: ZFieldHeightSchema,
|
||||
}),
|
||||
)
|
||||
.array()
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.refine(
|
||||
(recipients) => {
|
||||
const emails = recipients.map((recipient) => recipient.email);
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
{ message: 'Recipients must have unique emails' },
|
||||
),
|
||||
meta: z
|
||||
.object({
|
||||
subject: ZDocumentMetaSubjectSchema.optional(),
|
||||
message: ZDocumentMetaMessageSchema.optional(),
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional(),
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
|
||||
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const ZUpdateEmbeddingDocumentResponseSchema = z.object({
|
||||
documentId: z.number(),
|
||||
});
|
||||
|
||||
export type TUpdateEmbeddingDocumentRequestSchema = z.infer<
|
||||
typeof ZUpdateEmbeddingDocumentRequestSchema
|
||||
>;
|
||||
@ -0,0 +1,110 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
|
||||
import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template';
|
||||
import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients';
|
||||
import { updateTemplate } from '@documenso/lib/server-only/template/update-template';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
|
||||
import { procedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateEmbeddingTemplateRequestSchema,
|
||||
ZUpdateEmbeddingTemplateResponseSchema,
|
||||
} from './update-embedding-template.types';
|
||||
|
||||
export const updateEmbeddingTemplateRoute = procedure
|
||||
.input(ZUpdateEmbeddingTemplateRequestSchema)
|
||||
.output(ZUpdateEmbeddingTemplateResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
templateId: input.templateId,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const authorizationHeader = ctx.req.headers.get('authorization');
|
||||
|
||||
const [presignToken] = (authorizationHeader || '')
|
||||
.split('Bearer ')
|
||||
.filter((s) => s.length > 0);
|
||||
|
||||
if (!presignToken) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'No presign token provided',
|
||||
});
|
||||
}
|
||||
|
||||
const apiToken = await verifyEmbeddingPresignToken({ token: presignToken });
|
||||
|
||||
const { templateId, title, externalId, recipients, meta } = input;
|
||||
|
||||
await updateTemplate({
|
||||
templateId,
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId ?? undefined,
|
||||
data: {
|
||||
title,
|
||||
externalId,
|
||||
},
|
||||
meta,
|
||||
});
|
||||
|
||||
const recipientsWithClientId = recipients.map((recipient) => ({
|
||||
...recipient,
|
||||
clientId: nanoid(),
|
||||
}));
|
||||
|
||||
const { recipients: updatedRecipients } = await setTemplateRecipients({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId ?? undefined,
|
||||
templateId,
|
||||
recipients: recipientsWithClientId.map((recipient) => ({
|
||||
id: recipient.id,
|
||||
email: recipient.email,
|
||||
name: recipient.name ?? '',
|
||||
role: recipient.role ?? 'SIGNER',
|
||||
signingOrder: recipient.signingOrder,
|
||||
})),
|
||||
});
|
||||
|
||||
const fields = recipientsWithClientId.flatMap((recipient) => {
|
||||
const recipientId = updatedRecipients.find((r) => r.email === recipient.email)?.id;
|
||||
|
||||
if (!recipientId) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
return (recipient.fields ?? []).map((field) => ({
|
||||
...field,
|
||||
recipientId,
|
||||
// !: Temp property to be removed once we don't link based on signer email
|
||||
signerEmail: recipient.email,
|
||||
}));
|
||||
});
|
||||
|
||||
await setFieldsForTemplate({
|
||||
userId: apiToken.userId,
|
||||
teamId: apiToken.teamId ?? undefined,
|
||||
templateId,
|
||||
fields: fields.map((field) => ({
|
||||
...field,
|
||||
pageWidth: field.width,
|
||||
pageHeight: field.height,
|
||||
})),
|
||||
});
|
||||
|
||||
return {
|
||||
templateId,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Failed to update template',
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,77 @@
|
||||
import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||
import {
|
||||
ZFieldHeightSchema,
|
||||
ZFieldPageNumberSchema,
|
||||
ZFieldPageXSchema,
|
||||
ZFieldPageYSchema,
|
||||
ZFieldWidthSchema,
|
||||
} from '@documenso/lib/types/field';
|
||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
|
||||
import {
|
||||
ZDocumentMetaDateFormatSchema,
|
||||
ZDocumentMetaDistributionMethodSchema,
|
||||
ZDocumentMetaDrawSignatureEnabledSchema,
|
||||
ZDocumentMetaLanguageSchema,
|
||||
ZDocumentMetaMessageSchema,
|
||||
ZDocumentMetaRedirectUrlSchema,
|
||||
ZDocumentMetaSubjectSchema,
|
||||
ZDocumentMetaTimezoneSchema,
|
||||
ZDocumentMetaTypedSignatureEnabledSchema,
|
||||
ZDocumentMetaUploadSignatureEnabledSchema,
|
||||
ZDocumentTitleSchema,
|
||||
} from '../document-router/schema';
|
||||
|
||||
const ZFieldSchema = z.object({
|
||||
id: z.number().optional(),
|
||||
type: z.nativeEnum(FieldType),
|
||||
pageNumber: ZFieldPageNumberSchema,
|
||||
pageX: ZFieldPageXSchema,
|
||||
pageY: ZFieldPageYSchema,
|
||||
width: ZFieldWidthSchema,
|
||||
height: ZFieldHeightSchema,
|
||||
fieldMeta: ZFieldMetaSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
|
||||
templateId: z.number(),
|
||||
title: ZDocumentTitleSchema.optional(),
|
||||
externalId: z.string().optional(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
email: z.string().email(),
|
||||
name: z.string().optional(),
|
||||
role: z.nativeEnum(RecipientRole).optional(),
|
||||
signingOrder: z.number().optional(),
|
||||
fields: z.array(ZFieldSchema).optional(),
|
||||
}),
|
||||
),
|
||||
meta: z
|
||||
.object({
|
||||
subject: ZDocumentMetaSubjectSchema.optional(),
|
||||
message: ZDocumentMetaMessageSchema.optional(),
|
||||
timezone: ZDocumentMetaTimezoneSchema.optional(),
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
|
||||
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
|
||||
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
|
||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const ZUpdateEmbeddingTemplateResponseSchema = z.object({
|
||||
templateId: z.number(),
|
||||
});
|
||||
|
||||
export type TUpdateEmbeddingTemplateRequestSchema = z.infer<
|
||||
typeof ZUpdateEmbeddingTemplateRequestSchema
|
||||
>;
|
||||
@ -0,0 +1,66 @@
|
||||
import { createEmailDomain } from '@documenso/ee/server-only/lib/create-email-domain';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZCreateOrganisationEmailDomainRequestSchema,
|
||||
ZCreateOrganisationEmailDomainResponseSchema,
|
||||
} from './create-organisation-email-domain.types';
|
||||
|
||||
export const createOrganisationEmailDomainRoute = authenticatedProcedure
|
||||
.input(ZCreateOrganisationEmailDomainRequestSchema)
|
||||
.output(ZCreateOrganisationEmailDomainResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { organisationId, domain } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
domain,
|
||||
},
|
||||
});
|
||||
|
||||
if (!IS_BILLING_ENABLED()) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Billing is not enabled',
|
||||
});
|
||||
}
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId: user.id,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
include: {
|
||||
emailDomains: true,
|
||||
organisationClaim: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (!organisation.organisationClaim.flags.emailDomains) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Email domains are not enabled for this organisation',
|
||||
});
|
||||
}
|
||||
|
||||
if (organisation.emailDomains.length >= 100) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'You have reached the maximum number of email domains',
|
||||
});
|
||||
}
|
||||
|
||||
return await createEmailDomain({
|
||||
domain,
|
||||
organisationId,
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,27 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZEmailDomainSchema } from '@documenso/lib/types/email-domain';
|
||||
|
||||
const domainRegex =
|
||||
/^(?!https?:\/\/)(?!www\.)([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
||||
|
||||
export const ZDomainSchema = z
|
||||
.string()
|
||||
.regex(domainRegex, { message: 'Invalid domain name' })
|
||||
.toLowerCase();
|
||||
|
||||
export const ZCreateOrganisationEmailDomainRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
domain: ZDomainSchema,
|
||||
});
|
||||
|
||||
export const ZCreateOrganisationEmailDomainResponseSchema = z.object({
|
||||
emailDomain: ZEmailDomainSchema,
|
||||
records: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
type: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
@ -0,0 +1,61 @@
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { generateDatabaseId } from '@documenso/lib/universal/id';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZCreateOrganisationEmailRequestSchema,
|
||||
ZCreateOrganisationEmailResponseSchema,
|
||||
} from './create-organisation-email.types';
|
||||
|
||||
export const createOrganisationEmailRoute = authenticatedProcedure
|
||||
.input(ZCreateOrganisationEmailRequestSchema)
|
||||
.output(ZCreateOrganisationEmailResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { email, emailName, emailDomainId } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
emailDomainId,
|
||||
},
|
||||
});
|
||||
|
||||
const emailDomain = await prisma.emailDomain.findFirst({
|
||||
where: {
|
||||
id: emailDomainId,
|
||||
organisation: buildOrganisationWhereQuery({
|
||||
organisationId: undefined,
|
||||
userId: user.id,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!emailDomain) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Email domain not found',
|
||||
});
|
||||
}
|
||||
|
||||
const allowedEmailSuffix = '@' + emailDomain.domain;
|
||||
|
||||
if (!email.endsWith(allowedEmailSuffix)) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Cannot create an email with a different domain',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.organisationEmail.create({
|
||||
data: {
|
||||
id: generateDatabaseId('org_email'),
|
||||
organisationId: emailDomain.organisationId,
|
||||
emailName,
|
||||
// replyTo,
|
||||
email,
|
||||
emailDomainId,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZCreateOrganisationEmailRequestSchema = z.object({
|
||||
emailDomainId: z.string(),
|
||||
emailName: z.string().min(1).max(100),
|
||||
email: z.string().email().toLowerCase(),
|
||||
|
||||
// This does not need to be validated to be part of the domain.
|
||||
// replyTo: z.string().email().optional(),
|
||||
});
|
||||
|
||||
export const ZCreateOrganisationEmailResponseSchema = z.void();
|
||||
@ -0,0 +1,92 @@
|
||||
import { createCheckoutSession } from '@documenso/ee/server-only/stripe/create-checkout-session';
|
||||
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
|
||||
import { IS_BILLING_ENABLED, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZCreateSubscriptionRequestSchema } from './create-subscription.types';
|
||||
|
||||
export const createSubscriptionRoute = authenticatedProcedure
|
||||
.input(ZCreateSubscriptionRequestSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { organisationId, priceId, isPersonalLayoutMode } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
priceId,
|
||||
},
|
||||
});
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
if (!IS_BILLING_ENABLED()) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Billing is not enabled',
|
||||
});
|
||||
}
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_BILLING'],
|
||||
}),
|
||||
include: {
|
||||
subscription: true,
|
||||
owner: {
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
let customerId = organisation.customerId;
|
||||
|
||||
if (!customerId) {
|
||||
const customer = await createCustomer({
|
||||
name: organisation.owner.name || organisation.owner.email,
|
||||
email: organisation.owner.email,
|
||||
});
|
||||
|
||||
customerId = customer.id;
|
||||
|
||||
await prisma.organisation.update({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
data: {
|
||||
customerId: customer.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const returnUrl = isPersonalLayoutMode
|
||||
? `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`
|
||||
: `${NEXT_PUBLIC_WEBAPP_URL()}/o/${organisation.url}/settings/billing`;
|
||||
|
||||
const redirectUrl = await createCheckoutSession({
|
||||
customerId,
|
||||
priceId,
|
||||
returnUrl,
|
||||
});
|
||||
|
||||
if (!redirectUrl) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Failed to create checkout session',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
redirectUrl,
|
||||
};
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZCreateSubscriptionRequestSchema = z.object({
|
||||
organisationId: z.string().describe('The organisation to create the subscription for'),
|
||||
priceId: z.string().describe('The price to create the subscription for'),
|
||||
isPersonalLayoutMode: z.boolean().optional(),
|
||||
});
|
||||
@ -0,0 +1,53 @@
|
||||
import { deleteEmailDomain } from '@documenso/ee/server-only/lib/delete-email-domain';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZDeleteOrganisationEmailDomainRequestSchema,
|
||||
ZDeleteOrganisationEmailDomainResponseSchema,
|
||||
} from './delete-organisation-email-domain.types';
|
||||
|
||||
export const deleteOrganisationEmailDomainRoute = authenticatedProcedure
|
||||
.input(ZDeleteOrganisationEmailDomainRequestSchema)
|
||||
.output(ZDeleteOrganisationEmailDomainResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { emailDomainId } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
emailDomainId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!IS_BILLING_ENABLED()) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Billing is not enabled',
|
||||
});
|
||||
}
|
||||
|
||||
const emailDomain = await prisma.emailDomain.findFirst({
|
||||
where: {
|
||||
id: emailDomainId,
|
||||
organisation: buildOrganisationWhereQuery({
|
||||
organisationId: undefined,
|
||||
userId: user.id,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!emailDomain) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Email domain not found',
|
||||
});
|
||||
}
|
||||
|
||||
await deleteEmailDomain({
|
||||
emailDomainId: emailDomain.id,
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZDeleteOrganisationEmailDomainRequestSchema = z.object({
|
||||
emailDomainId: z.string(),
|
||||
});
|
||||
|
||||
export const ZDeleteOrganisationEmailDomainResponseSchema = z.void();
|
||||
@ -0,0 +1,45 @@
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZDeleteOrganisationEmailRequestSchema,
|
||||
ZDeleteOrganisationEmailResponseSchema,
|
||||
} from './delete-organisation-email.types';
|
||||
|
||||
export const deleteOrganisationEmailRoute = authenticatedProcedure
|
||||
.input(ZDeleteOrganisationEmailRequestSchema)
|
||||
.output(ZDeleteOrganisationEmailResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { emailId } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
emailId,
|
||||
},
|
||||
});
|
||||
|
||||
const email = await prisma.organisationEmail.findFirst({
|
||||
where: {
|
||||
id: emailId,
|
||||
organisation: buildOrganisationWhereQuery({
|
||||
organisationId: undefined,
|
||||
userId: user.id,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
await prisma.organisationEmail.delete({
|
||||
where: {
|
||||
id: email.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZDeleteOrganisationEmailRequestSchema = z.object({
|
||||
emailId: z.string(),
|
||||
});
|
||||
|
||||
export const ZDeleteOrganisationEmailResponseSchema = z.void();
|
||||
@ -0,0 +1,122 @@
|
||||
import type { EmailDomainStatus } from '@prisma/client';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { FindResultResponse } from '@documenso/lib/types/search-params';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZFindOrganisationEmailDomainsRequestSchema,
|
||||
ZFindOrganisationEmailDomainsResponseSchema,
|
||||
} from './find-organisation-email-domain.types';
|
||||
|
||||
export const findOrganisationEmailDomainsRoute = authenticatedProcedure
|
||||
.input(ZFindOrganisationEmailDomainsRequestSchema)
|
||||
.output(ZFindOrganisationEmailDomainsResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { organisationId, emailDomainId, statuses, query, page, perPage } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
return await findOrganisationEmailDomains({
|
||||
userId: user.id,
|
||||
organisationId,
|
||||
emailDomainId,
|
||||
statuses,
|
||||
query,
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
});
|
||||
|
||||
type FindOrganisationEmailDomainsOptions = {
|
||||
userId: number;
|
||||
organisationId: string;
|
||||
emailDomainId?: string;
|
||||
statuses?: EmailDomainStatus[];
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
};
|
||||
|
||||
export const findOrganisationEmailDomains = async ({
|
||||
userId,
|
||||
organisationId,
|
||||
emailDomainId,
|
||||
statuses = [],
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 100,
|
||||
}: FindOrganisationEmailDomainsOptions) => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({ organisationId, userId }),
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const whereClause: Prisma.EmailDomainWhereInput = {
|
||||
organisationId: organisation.id,
|
||||
status: statuses.length > 0 ? { in: statuses } : undefined,
|
||||
};
|
||||
|
||||
if (emailDomainId) {
|
||||
whereClause.id = emailDomainId;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
whereClause.domain = {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
};
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.emailDomain.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
organisationId: true,
|
||||
domain: true,
|
||||
selector: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
emails: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.emailDomain.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
const mappedData = data.map((item) => ({
|
||||
...item,
|
||||
emailCount: item._count.emails,
|
||||
}));
|
||||
|
||||
return {
|
||||
data: mappedData,
|
||||
count,
|
||||
currentPage: page,
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof mappedData>;
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { EmailDomainStatus } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZEmailDomainManySchema } from '@documenso/lib/types/email-domain';
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
|
||||
export const ZFindOrganisationEmailDomainsRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
organisationId: z.string(),
|
||||
emailDomainId: z.string().optional(),
|
||||
statuses: z.nativeEnum(EmailDomainStatus).array().optional(),
|
||||
});
|
||||
|
||||
export const ZFindOrganisationEmailDomainsResponseSchema = ZFindResultResponse.extend({
|
||||
data: z.array(
|
||||
ZEmailDomainManySchema.extend({
|
||||
emailCount: z.number(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type TFindOrganisationEmailDomainsResponse = z.infer<
|
||||
typeof ZFindOrganisationEmailDomainsResponseSchema
|
||||
>;
|
||||
@ -0,0 +1,105 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { FindResultResponse } from '@documenso/lib/types/search-params';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZFindOrganisationEmailsRequestSchema,
|
||||
ZFindOrganisationEmailsResponseSchema,
|
||||
} from './find-organisation-emails.types';
|
||||
|
||||
export const findOrganisationEmailsRoute = authenticatedProcedure
|
||||
.input(ZFindOrganisationEmailsRequestSchema)
|
||||
.output(ZFindOrganisationEmailsResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { organisationId, emailDomainId, query, page, perPage } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
return await findOrganisationEmails({
|
||||
userId: user.id,
|
||||
organisationId,
|
||||
emailDomainId,
|
||||
query,
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
});
|
||||
|
||||
type FindOrganisationEmailsOptions = {
|
||||
userId: number;
|
||||
organisationId: string;
|
||||
emailDomainId?: string;
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
};
|
||||
|
||||
export const findOrganisationEmails = async ({
|
||||
userId,
|
||||
organisationId,
|
||||
emailDomainId,
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 100,
|
||||
}: FindOrganisationEmailsOptions) => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({ organisationId, userId }),
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const whereClause: Prisma.OrganisationEmailWhereInput = {
|
||||
organisationId: organisation.id,
|
||||
emailDomainId,
|
||||
};
|
||||
|
||||
if (query) {
|
||||
whereClause.email = {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
};
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.organisationEmail.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
email: true,
|
||||
emailName: true,
|
||||
// replyTo: true,
|
||||
emailDomainId: true,
|
||||
organisationId: true,
|
||||
},
|
||||
}),
|
||||
prisma.organisationEmail.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
count,
|
||||
currentPage: page,
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZOrganisationEmailManySchema } from '@documenso/lib/types/organisation-email';
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
|
||||
export const ZFindOrganisationEmailsRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
organisationId: z.string(),
|
||||
emailDomainId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ZFindOrganisationEmailsResponseSchema = ZFindResultResponse.extend({
|
||||
data: ZOrganisationEmailManySchema.array(),
|
||||
});
|
||||
|
||||
export type TFindOrganisationEmailsResponse = z.infer<typeof ZFindOrganisationEmailsResponseSchema>;
|
||||
64
packages/trpc/server/enterprise-router/get-invoices.ts
Normal file
64
packages/trpc/server/enterprise-router/get-invoices.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { getInvoices } from '@documenso/ee/server-only/stripe/get-invoices';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZGetInvoicesRequestSchema } from './get-invoices.types';
|
||||
|
||||
export const getInvoicesRoute = authenticatedProcedure
|
||||
.input(ZGetInvoicesRequestSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { organisationId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
if (!IS_BILLING_ENABLED()) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Billing is not enabled',
|
||||
});
|
||||
}
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
include: {
|
||||
subscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You are not authorized to access this organisation',
|
||||
});
|
||||
}
|
||||
|
||||
if (!organisation.customerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const invoices = await getInvoices({
|
||||
customerId: organisation.customerId,
|
||||
});
|
||||
|
||||
return invoices.data.map((invoice) => ({
|
||||
id: invoice.id,
|
||||
status: invoice.status,
|
||||
created: invoice.created,
|
||||
currency: invoice.currency,
|
||||
total: invoice.total,
|
||||
hosted_invoice_url: invoice.hosted_invoice_url,
|
||||
invoice_pdf: invoice.invoice_pdf,
|
||||
}));
|
||||
});
|
||||
@ -0,0 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZGetInvoicesRequestSchema = z.object({
|
||||
organisationId: z.string().describe('The organisation to get the invoices for'),
|
||||
});
|
||||
@ -0,0 +1,63 @@
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetOrganisationEmailDomainRequestSchema,
|
||||
ZGetOrganisationEmailDomainResponseSchema,
|
||||
} from './get-organisation-email-domain.types';
|
||||
|
||||
export const getOrganisationEmailDomainRoute = authenticatedProcedure
|
||||
.input(ZGetOrganisationEmailDomainRequestSchema)
|
||||
.output(ZGetOrganisationEmailDomainResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { emailDomainId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
emailDomainId,
|
||||
},
|
||||
});
|
||||
|
||||
return await getOrganisationEmailDomain({
|
||||
userId: ctx.user.id,
|
||||
emailDomainId,
|
||||
});
|
||||
});
|
||||
|
||||
type GetOrganisationEmailDomainOptions = {
|
||||
userId: number;
|
||||
emailDomainId: string;
|
||||
};
|
||||
|
||||
export const getOrganisationEmailDomain = async ({
|
||||
userId,
|
||||
emailDomainId,
|
||||
}: GetOrganisationEmailDomainOptions) => {
|
||||
const emailDomain = await prisma.emailDomain.findFirst({
|
||||
where: {
|
||||
id: emailDomainId,
|
||||
organisation: buildOrganisationWhereQuery({
|
||||
organisationId: undefined,
|
||||
userId,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
},
|
||||
omit: {
|
||||
privateKey: true,
|
||||
},
|
||||
include: {
|
||||
emails: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!emailDomain) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Email domain not found',
|
||||
});
|
||||
}
|
||||
|
||||
return emailDomain;
|
||||
};
|
||||
@ -0,0 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZEmailDomainSchema } from '@documenso/lib/types/email-domain';
|
||||
|
||||
export const ZGetOrganisationEmailDomainRequestSchema = z.object({
|
||||
emailDomainId: z.string(),
|
||||
});
|
||||
|
||||
export const ZGetOrganisationEmailDomainResponseSchema = ZEmailDomainSchema;
|
||||
|
||||
export type TGetOrganisationEmailDomainResponse = z.infer<
|
||||
typeof ZGetOrganisationEmailDomainResponseSchema
|
||||
>;
|
||||
31
packages/trpc/server/enterprise-router/get-plans.ts
Normal file
31
packages/trpc/server/enterprise-router/get-plans.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { getInternalClaimPlans } from '@documenso/ee/server-only/stripe/get-internal-claim-plans';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
|
||||
export const getPlansRoute = authenticatedProcedure.query(async ({ ctx }) => {
|
||||
const userId = ctx.user.id;
|
||||
|
||||
const plans = await getInternalClaimPlans();
|
||||
|
||||
let canCreateFreeOrganisation = false;
|
||||
|
||||
if (IS_BILLING_ENABLED()) {
|
||||
const numberOfFreeOrganisations = await prisma.organisation.count({
|
||||
where: {
|
||||
ownerUserId: userId,
|
||||
subscription: {
|
||||
is: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
canCreateFreeOrganisation = numberOfFreeOrganisations === 0;
|
||||
}
|
||||
|
||||
return {
|
||||
plans,
|
||||
canCreateFreeOrganisation,
|
||||
};
|
||||
});
|
||||
40
packages/trpc/server/enterprise-router/get-subscription.ts
Normal file
40
packages/trpc/server/enterprise-router/get-subscription.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { getInternalClaimPlans } from '@documenso/ee/server-only/stripe/get-internal-claim-plans';
|
||||
import { getSubscription } from '@documenso/ee/server-only/stripe/get-subscription';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZGetSubscriptionRequestSchema } from './get-subscription.types';
|
||||
|
||||
export const getSubscriptionRoute = authenticatedProcedure
|
||||
.input(ZGetSubscriptionRequestSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { organisationId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
if (!IS_BILLING_ENABLED()) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Billing is not enabled',
|
||||
});
|
||||
}
|
||||
|
||||
const [subscription, plans] = await Promise.all([
|
||||
getSubscription({
|
||||
organisationId,
|
||||
userId,
|
||||
}),
|
||||
getInternalClaimPlans(),
|
||||
]);
|
||||
|
||||
return {
|
||||
subscription,
|
||||
plans,
|
||||
};
|
||||
});
|
||||
@ -0,0 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZGetSubscriptionRequestSchema = z.object({
|
||||
organisationId: z.string().describe('The organisation to get the subscription for'),
|
||||
});
|
||||
104
packages/trpc/server/enterprise-router/manage-subscription.ts
Normal file
104
packages/trpc/server/enterprise-router/manage-subscription.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
|
||||
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
||||
import { IS_BILLING_ENABLED, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZManageSubscriptionRequestSchema } from './manage-subscription.types';
|
||||
|
||||
export const manageSubscriptionRoute = authenticatedProcedure
|
||||
.input(ZManageSubscriptionRequestSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { organisationId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
if (!IS_BILLING_ENABLED()) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Billing is not enabled',
|
||||
});
|
||||
}
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_BILLING'],
|
||||
}),
|
||||
include: {
|
||||
subscription: true,
|
||||
owner: {
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
let customerId = organisation.customerId;
|
||||
|
||||
// If for some reason customer ID is missing in the organisation but
|
||||
// exists in the subscription take it from the subscription.
|
||||
if (!customerId && organisation.subscription?.customerId) {
|
||||
customerId = organisation.subscription.customerId;
|
||||
|
||||
await prisma.organisation
|
||||
.update({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
data: {
|
||||
customerId,
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
// Todo: Logger
|
||||
console.error('Critical error, potential conflicting data');
|
||||
console.error(err.message);
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// If the customer ID is still missing create a new customer.
|
||||
if (!customerId) {
|
||||
const customer = await createCustomer({
|
||||
name: organisation.owner.name || organisation.owner.email,
|
||||
email: organisation.owner.email,
|
||||
});
|
||||
|
||||
customerId = customer.id;
|
||||
|
||||
await prisma.organisation.update({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
data: {
|
||||
customerId: customer.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const redirectUrl = await getPortalSession({
|
||||
customerId,
|
||||
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/o/${organisation.url}/settings/billing`,
|
||||
});
|
||||
|
||||
return {
|
||||
redirectUrl,
|
||||
};
|
||||
});
|
||||
@ -0,0 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZManageSubscriptionRequestSchema = z.object({
|
||||
organisationId: z.string().describe('The organisation to manage the subscription for'),
|
||||
});
|
||||
46
packages/trpc/server/enterprise-router/router.ts
Normal file
46
packages/trpc/server/enterprise-router/router.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { router } from '../trpc';
|
||||
import { createOrganisationEmailRoute } from './create-organisation-email';
|
||||
import { createOrganisationEmailDomainRoute } from './create-organisation-email-domain';
|
||||
import { createSubscriptionRoute } from './create-subscription';
|
||||
import { deleteOrganisationEmailRoute } from './delete-organisation-email';
|
||||
import { deleteOrganisationEmailDomainRoute } from './delete-organisation-email-domain';
|
||||
import { findOrganisationEmailDomainsRoute } from './find-organisation-email-domain';
|
||||
import { findOrganisationEmailsRoute } from './find-organisation-emails';
|
||||
import { getInvoicesRoute } from './get-invoices';
|
||||
import { getOrganisationEmailDomainRoute } from './get-organisation-email-domain';
|
||||
import { getPlansRoute } from './get-plans';
|
||||
import { getSubscriptionRoute } from './get-subscription';
|
||||
import { manageSubscriptionRoute } from './manage-subscription';
|
||||
import { updateOrganisationEmailRoute } from './update-organisation-email';
|
||||
import { verifyOrganisationEmailDomainRoute } from './verify-organisation-email-domain';
|
||||
|
||||
export const enterpriseRouter = router({
|
||||
organisation: {
|
||||
email: {
|
||||
find: findOrganisationEmailsRoute,
|
||||
create: createOrganisationEmailRoute,
|
||||
update: updateOrganisationEmailRoute,
|
||||
delete: deleteOrganisationEmailRoute,
|
||||
},
|
||||
emailDomain: {
|
||||
get: getOrganisationEmailDomainRoute,
|
||||
find: findOrganisationEmailDomainsRoute,
|
||||
create: createOrganisationEmailDomainRoute,
|
||||
delete: deleteOrganisationEmailDomainRoute,
|
||||
verify: verifyOrganisationEmailDomainRoute,
|
||||
},
|
||||
},
|
||||
billing: {
|
||||
plans: {
|
||||
get: getPlansRoute,
|
||||
},
|
||||
subscription: {
|
||||
get: getSubscriptionRoute,
|
||||
create: createSubscriptionRoute,
|
||||
manage: manageSubscriptionRoute,
|
||||
},
|
||||
invoices: {
|
||||
get: getInvoicesRoute,
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,49 @@
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateOrganisationEmailRequestSchema,
|
||||
ZUpdateOrganisationEmailResponseSchema,
|
||||
} from './update-organisation-email.types';
|
||||
|
||||
export const updateOrganisationEmailRoute = authenticatedProcedure
|
||||
.input(ZUpdateOrganisationEmailRequestSchema)
|
||||
.output(ZUpdateOrganisationEmailResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { emailId, emailName } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
emailId,
|
||||
},
|
||||
});
|
||||
|
||||
const organisationEmail = await prisma.organisationEmail.findFirst({
|
||||
where: {
|
||||
id: emailId,
|
||||
organisation: buildOrganisationWhereQuery({
|
||||
organisationId: undefined,
|
||||
userId: user.id,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisationEmail) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
await prisma.organisationEmail.update({
|
||||
where: {
|
||||
id: emailId,
|
||||
},
|
||||
data: {
|
||||
emailName,
|
||||
// replyTo,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZCreateOrganisationEmailRequestSchema } from './create-organisation-email.types';
|
||||
|
||||
export const ZUpdateOrganisationEmailRequestSchema = z
|
||||
.object({
|
||||
emailId: z.string(),
|
||||
})
|
||||
.extend(
|
||||
ZCreateOrganisationEmailRequestSchema.pick({
|
||||
emailName: true,
|
||||
// replyTo: true
|
||||
}).shape,
|
||||
);
|
||||
|
||||
export const ZUpdateOrganisationEmailResponseSchema = z.void();
|
||||
|
||||
export type TUpdateOrganisationEmailRequest = z.infer<typeof ZUpdateOrganisationEmailRequestSchema>;
|
||||
@ -0,0 +1,59 @@
|
||||
import { verifyEmailDomain } from '@documenso/ee/server-only/lib/verify-email-domain';
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZVerifyOrganisationEmailDomainRequestSchema,
|
||||
ZVerifyOrganisationEmailDomainResponseSchema,
|
||||
} from './verify-organisation-email-domain.types';
|
||||
|
||||
export const verifyOrganisationEmailDomainRoute = authenticatedProcedure
|
||||
.input(ZVerifyOrganisationEmailDomainRequestSchema)
|
||||
.output(ZVerifyOrganisationEmailDomainResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { organisationId, emailDomainId } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
emailDomainId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!IS_BILLING_ENABLED()) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Billing is not enabled',
|
||||
});
|
||||
}
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId: user.id,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
include: {
|
||||
emailDomains: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// Filter down emails to verify a specific email, otherwise verify all emails regardless of status.
|
||||
const emailsToVerify = organisation.emailDomains.filter((email) => {
|
||||
if (emailDomainId && email.id !== emailDomainId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
await Promise.all(emailsToVerify.map(async (email) => verifyEmailDomain(email.id)));
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZVerifyOrganisationEmailDomainRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
emailDomainId: z.string().optional().describe('Leave blank to revalidate all emails'),
|
||||
});
|
||||
|
||||
export const ZVerifyOrganisationEmailDomainResponseSchema = z.void();
|
||||
@ -62,6 +62,12 @@ export const fieldRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { fieldId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
fieldId,
|
||||
},
|
||||
});
|
||||
|
||||
return await getFieldById({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -88,6 +94,12 @@ export const fieldRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { documentId, field } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
const createdFields = await createDocumentFields({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -118,6 +130,12 @@ export const fieldRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { documentId, fields } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
return await createDocumentFields({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -146,6 +164,12 @@ export const fieldRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { documentId, field } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
const updatedFields = await updateDocumentFields({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -176,6 +200,12 @@ export const fieldRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { documentId, fields } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
return await updateDocumentFields({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -203,6 +233,12 @@ export const fieldRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { fieldId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
fieldId,
|
||||
},
|
||||
});
|
||||
|
||||
await deleteDocumentField({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -225,6 +261,12 @@ export const fieldRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { documentId, fields } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
},
|
||||
});
|
||||
|
||||
return await setFieldsForDocument({
|
||||
documentId,
|
||||
userId: ctx.user.id,
|
||||
@ -263,6 +305,12 @@ export const fieldRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { templateId, field } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
templateId,
|
||||
},
|
||||
});
|
||||
|
||||
const createdFields = await createTemplateFields({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -293,6 +341,12 @@ export const fieldRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { fieldId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
fieldId,
|
||||
},
|
||||
});
|
||||
|
||||
return await getFieldById({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -319,6 +373,12 @@ export const fieldRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { templateId, fields } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
templateId,
|
||||
},
|
||||
});
|
||||
|
||||
return await createTemplateFields({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -346,6 +406,12 @@ export const fieldRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { templateId, field } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
templateId,
|
||||
},
|
||||
});
|
||||
|
||||
const updatedFields = await updateTemplateFields({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -375,6 +441,12 @@ export const fieldRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { templateId, fields } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
templateId,
|
||||
},
|
||||
});
|
||||
|
||||
return await updateTemplateFields({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -401,6 +473,12 @@ export const fieldRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { fieldId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
fieldId,
|
||||
},
|
||||
});
|
||||
|
||||
await deleteTemplateField({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
@ -422,6 +500,12 @@ export const fieldRouter = router({
|
||||
const { teamId } = ctx;
|
||||
const { templateId, fields } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
templateId,
|
||||
},
|
||||
});
|
||||
|
||||
return await setFieldsForTemplate({
|
||||
templateId,
|
||||
userId: ctx.user.id,
|
||||
@ -448,6 +532,12 @@ export const fieldRouter = router({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { token, fieldId, value, isBase64, authOptions } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
fieldId,
|
||||
},
|
||||
});
|
||||
|
||||
return await signFieldWithToken({
|
||||
token,
|
||||
fieldId,
|
||||
@ -467,6 +557,12 @@ export const fieldRouter = router({
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { token, fieldId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
fieldId,
|
||||
},
|
||||
});
|
||||
|
||||
return await removeSignedFieldWithToken({
|
||||
token,
|
||||
fieldId,
|
||||
|
||||
419
packages/trpc/server/folder-router/router.ts
Normal file
419
packages/trpc/server/folder-router/router.ts
Normal file
@ -0,0 +1,419 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createFolder } from '@documenso/lib/server-only/folder/create-folder';
|
||||
import { deleteFolder } from '@documenso/lib/server-only/folder/delete-folder';
|
||||
import { findFolders } from '@documenso/lib/server-only/folder/find-folders';
|
||||
import { getFolderBreadcrumbs } from '@documenso/lib/server-only/folder/get-folder-breadcrumbs';
|
||||
import { getFolderById } from '@documenso/lib/server-only/folder/get-folder-by-id';
|
||||
import { moveDocumentToFolder } from '@documenso/lib/server-only/folder/move-document-to-folder';
|
||||
import { moveFolder } from '@documenso/lib/server-only/folder/move-folder';
|
||||
import { moveTemplateToFolder } from '@documenso/lib/server-only/folder/move-template-to-folder';
|
||||
import { pinFolder } from '@documenso/lib/server-only/folder/pin-folder';
|
||||
import { unpinFolder } from '@documenso/lib/server-only/folder/unpin-folder';
|
||||
import { updateFolder } from '@documenso/lib/server-only/folder/update-folder';
|
||||
import { FolderType } from '@documenso/lib/types/folder-type';
|
||||
|
||||
import { authenticatedProcedure, router } from '../trpc';
|
||||
import {
|
||||
ZCreateFolderSchema,
|
||||
ZDeleteFolderSchema,
|
||||
ZFindFoldersRequestSchema,
|
||||
ZFindFoldersResponseSchema,
|
||||
ZGenericSuccessResponse,
|
||||
ZGetFoldersResponseSchema,
|
||||
ZGetFoldersSchema,
|
||||
ZMoveDocumentToFolderSchema,
|
||||
ZMoveFolderSchema,
|
||||
ZMoveTemplateToFolderSchema,
|
||||
ZPinFolderSchema,
|
||||
ZSuccessResponseSchema,
|
||||
ZUnpinFolderSchema,
|
||||
ZUpdateFolderSchema,
|
||||
} from './schema';
|
||||
|
||||
export const folderRouter = router({
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getFolders: authenticatedProcedure
|
||||
.input(ZGetFoldersSchema)
|
||||
.output(ZGetFoldersResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { parentId, type } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
parentId,
|
||||
type,
|
||||
},
|
||||
});
|
||||
|
||||
const folders = await findFolders({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
parentId,
|
||||
type,
|
||||
});
|
||||
|
||||
const breadcrumbs = parentId
|
||||
? await getFolderBreadcrumbs({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
folderId: parentId,
|
||||
type,
|
||||
})
|
||||
: [];
|
||||
|
||||
return {
|
||||
folders,
|
||||
breadcrumbs,
|
||||
type,
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
findFolders: authenticatedProcedure
|
||||
.input(ZFindFoldersRequestSchema)
|
||||
.output(ZFindFoldersResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { parentId, type } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
parentId,
|
||||
type,
|
||||
},
|
||||
});
|
||||
|
||||
const folders = await findFolders({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
parentId,
|
||||
type,
|
||||
});
|
||||
|
||||
const breadcrumbs = parentId
|
||||
? await getFolderBreadcrumbs({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
folderId: parentId,
|
||||
type,
|
||||
})
|
||||
: [];
|
||||
|
||||
return {
|
||||
data: folders,
|
||||
breadcrumbs,
|
||||
type,
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
createFolder: authenticatedProcedure
|
||||
.input(ZCreateFolderSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { name, parentId, type } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
parentId,
|
||||
type,
|
||||
},
|
||||
});
|
||||
|
||||
if (parentId) {
|
||||
try {
|
||||
await getFolderById({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
folderId: parentId,
|
||||
type,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Parent folder not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = await createFolder({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
name,
|
||||
parentId,
|
||||
type,
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
type,
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
updateFolder: authenticatedProcedure
|
||||
.input(ZUpdateFolderSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { id, name, visibility } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
const currentFolder = await getFolderById({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
folderId: id,
|
||||
});
|
||||
|
||||
const result = await updateFolder({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
folderId: id,
|
||||
name,
|
||||
visibility,
|
||||
type: currentFolder.type,
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
type: currentFolder.type,
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
deleteFolder: authenticatedProcedure
|
||||
.input(ZDeleteFolderSchema)
|
||||
.output(ZSuccessResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { id } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
await deleteFolder({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
folderId: id,
|
||||
});
|
||||
|
||||
return ZGenericSuccessResponse;
|
||||
}),
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
moveFolder: authenticatedProcedure.input(ZMoveFolderSchema).mutation(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { id, parentId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
id,
|
||||
parentId,
|
||||
},
|
||||
});
|
||||
|
||||
const currentFolder = await getFolderById({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
folderId: id,
|
||||
});
|
||||
|
||||
if (parentId !== null) {
|
||||
try {
|
||||
await getFolderById({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
folderId: parentId,
|
||||
type: currentFolder.type,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Parent folder not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = await moveFolder({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
folderId: id,
|
||||
parentId,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
type: currentFolder.type,
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
moveDocumentToFolder: authenticatedProcedure
|
||||
.input(ZMoveDocumentToFolderSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { documentId, folderId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
documentId,
|
||||
folderId,
|
||||
},
|
||||
});
|
||||
|
||||
if (folderId !== null) {
|
||||
try {
|
||||
await getFolderById({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
folderId,
|
||||
type: FolderType.DOCUMENT,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = await moveDocumentToFolder({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
documentId,
|
||||
folderId,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
type: FolderType.DOCUMENT,
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
moveTemplateToFolder: authenticatedProcedure
|
||||
.input(ZMoveTemplateToFolderSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId, user } = ctx;
|
||||
const { templateId, folderId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
templateId,
|
||||
folderId,
|
||||
},
|
||||
});
|
||||
|
||||
if (folderId !== null) {
|
||||
try {
|
||||
await getFolderById({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
folderId,
|
||||
type: FolderType.TEMPLATE,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Folder not found',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = await moveTemplateToFolder({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
templateId,
|
||||
folderId,
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
type: FolderType.TEMPLATE,
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
pinFolder: authenticatedProcedure.input(ZPinFolderSchema).mutation(async ({ ctx, input }) => {
|
||||
const { folderId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
folderId,
|
||||
},
|
||||
});
|
||||
|
||||
const currentFolder = await getFolderById({
|
||||
userId: ctx.user.id,
|
||||
teamId: ctx.teamId,
|
||||
folderId,
|
||||
});
|
||||
|
||||
const result = await pinFolder({
|
||||
userId: ctx.user.id,
|
||||
teamId: ctx.teamId,
|
||||
folderId,
|
||||
type: currentFolder.type,
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
type: currentFolder.type,
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
unpinFolder: authenticatedProcedure.input(ZUnpinFolderSchema).mutation(async ({ ctx, input }) => {
|
||||
const { folderId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
folderId,
|
||||
},
|
||||
});
|
||||
|
||||
const currentFolder = await getFolderById({
|
||||
userId: ctx.user.id,
|
||||
teamId: ctx.teamId,
|
||||
folderId,
|
||||
});
|
||||
|
||||
const result = await unpinFolder({
|
||||
userId: ctx.user.id,
|
||||
teamId: ctx.teamId,
|
||||
folderId,
|
||||
type: currentFolder.type,
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
type: currentFolder.type,
|
||||
};
|
||||
}),
|
||||
});
|
||||
126
packages/trpc/server/folder-router/schema.ts
Normal file
126
packages/trpc/server/folder-router/schema.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFolderTypeSchema } from '@documenso/lib/types/folder-type';
|
||||
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { DocumentVisibility } from '@documenso/prisma/generated/types';
|
||||
|
||||
/**
|
||||
* Required for empty responses since we currently can't 201 requests for our openapi setup.
|
||||
*
|
||||
* Without this it will throw an error in Speakeasy SDK when it tries to parse an empty response.
|
||||
*/
|
||||
export const ZSuccessResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
type: ZFolderTypeSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZGenericSuccessResponse = {
|
||||
success: true,
|
||||
} satisfies z.infer<typeof ZSuccessResponseSchema>;
|
||||
|
||||
export const ZFolderSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
userId: z.number(),
|
||||
teamId: z.number().nullable(),
|
||||
parentId: z.string().nullable(),
|
||||
pinned: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
visibility: z.nativeEnum(DocumentVisibility),
|
||||
type: ZFolderTypeSchema,
|
||||
});
|
||||
|
||||
export type TFolder = z.infer<typeof ZFolderSchema>;
|
||||
|
||||
const ZFolderCountSchema = z.object({
|
||||
documents: z.number(),
|
||||
templates: z.number(),
|
||||
subfolders: z.number(),
|
||||
});
|
||||
|
||||
const ZSubfolderSchema = ZFolderSchema.extend({
|
||||
subfolders: z.array(z.any()),
|
||||
_count: ZFolderCountSchema,
|
||||
});
|
||||
|
||||
export const ZFolderWithSubfoldersSchema = ZFolderSchema.extend({
|
||||
subfolders: z.array(ZSubfolderSchema),
|
||||
_count: ZFolderCountSchema,
|
||||
});
|
||||
|
||||
export type TFolderWithSubfolders = z.infer<typeof ZFolderWithSubfoldersSchema>;
|
||||
|
||||
export const ZCreateFolderSchema = z.object({
|
||||
name: z.string(),
|
||||
parentId: z.string().optional(),
|
||||
type: ZFolderTypeSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZUpdateFolderSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
visibility: z.nativeEnum(DocumentVisibility),
|
||||
type: ZFolderTypeSchema.optional(),
|
||||
});
|
||||
|
||||
export type TUpdateFolderSchema = z.infer<typeof ZUpdateFolderSchema>;
|
||||
|
||||
export const ZDeleteFolderSchema = z.object({
|
||||
id: z.string(),
|
||||
type: ZFolderTypeSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZMoveFolderSchema = z.object({
|
||||
id: z.string(),
|
||||
parentId: z.string().nullable(),
|
||||
type: ZFolderTypeSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZMoveDocumentToFolderSchema = z.object({
|
||||
documentId: z.number(),
|
||||
folderId: z.string().nullable().optional(),
|
||||
type: z.enum(['DOCUMENT']).optional(),
|
||||
});
|
||||
|
||||
export const ZMoveTemplateToFolderSchema = z.object({
|
||||
templateId: z.number(),
|
||||
folderId: z.string().nullable().optional(),
|
||||
type: z.enum(['TEMPLATE']).optional(),
|
||||
});
|
||||
|
||||
export const ZPinFolderSchema = z.object({
|
||||
folderId: z.string(),
|
||||
type: ZFolderTypeSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZUnpinFolderSchema = z.object({
|
||||
folderId: z.string(),
|
||||
type: ZFolderTypeSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZGetFoldersSchema = z.object({
|
||||
parentId: z.string().nullable().optional(),
|
||||
type: ZFolderTypeSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZGetFoldersResponseSchema = z.object({
|
||||
folders: z.array(ZFolderWithSubfoldersSchema),
|
||||
breadcrumbs: z.array(ZFolderSchema),
|
||||
type: ZFolderTypeSchema.optional(),
|
||||
});
|
||||
|
||||
export type TGetFoldersResponse = z.infer<typeof ZGetFoldersResponseSchema>;
|
||||
|
||||
export const ZFindFoldersRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
parentId: z.string().nullable().optional(),
|
||||
type: ZFolderTypeSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZFindFoldersResponseSchema = z.object({
|
||||
data: z.array(ZFolderWithSubfoldersSchema),
|
||||
breadcrumbs: z.array(ZFolderSchema),
|
||||
type: ZFolderTypeSchema.optional(),
|
||||
});
|
||||
|
||||
export type TFindFoldersResponse = z.infer<typeof ZFindFoldersResponseSchema>;
|
||||
@ -0,0 +1,18 @@
|
||||
import { acceptOrganisationInvitation } from '@documenso/lib/server-only/organisation/accept-organisation-invitation';
|
||||
|
||||
import { maybeAuthenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZAcceptOrganisationMemberInviteRequestSchema,
|
||||
ZAcceptOrganisationMemberInviteResponseSchema,
|
||||
} from './accept-organisation-member-invite.types';
|
||||
|
||||
export const acceptOrganisationMemberInviteRoute = maybeAuthenticatedProcedure
|
||||
.input(ZAcceptOrganisationMemberInviteRequestSchema)
|
||||
.output(ZAcceptOrganisationMemberInviteResponseSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const { token } = input;
|
||||
|
||||
return await acceptOrganisationInvitation({
|
||||
token,
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZAcceptOrganisationMemberInviteRequestSchema = z.object({
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
export const ZAcceptOrganisationMemberInviteResponseSchema = z.void();
|
||||
|
||||
export type TAcceptOrganisationMemberInviteResponse = z.infer<
|
||||
typeof ZAcceptOrganisationMemberInviteResponseSchema
|
||||
>;
|
||||
@ -0,0 +1,97 @@
|
||||
import { OrganisationGroupType } from '@prisma/client';
|
||||
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getMemberOrganisationRole } from '@documenso/lib/server-only/team/get-member-roles';
|
||||
import { generateDatabaseId } from '@documenso/lib/universal/id';
|
||||
import {
|
||||
buildOrganisationWhereQuery,
|
||||
isOrganisationRoleWithinUserHierarchy,
|
||||
} from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZCreateOrganisationGroupRequestSchema,
|
||||
ZCreateOrganisationGroupResponseSchema,
|
||||
} from './create-organisation-group.types';
|
||||
|
||||
export const createOrganisationGroupRoute = authenticatedProcedure
|
||||
// .meta(createOrganisationGroupMeta)
|
||||
.input(ZCreateOrganisationGroupRequestSchema)
|
||||
.output(ZCreateOrganisationGroupResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { organisationId, organisationRole, name, memberIds } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId: user.id,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
include: {
|
||||
groups: true,
|
||||
members: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const currentUserOrganisationRole = await getMemberOrganisationRole({
|
||||
organisationId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isOrganisationRoleWithinUserHierarchy(currentUserOrganisationRole, organisationRole)) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You are not allowed to create this organisation group',
|
||||
});
|
||||
}
|
||||
|
||||
// Validate that members exist in the organisation.
|
||||
memberIds.forEach((memberId) => {
|
||||
const member = organisation.members.find(({ id }) => id === memberId);
|
||||
|
||||
if (!member) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
});
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const group = await tx.organisationGroup.create({
|
||||
data: {
|
||||
id: generateDatabaseId('org_group'),
|
||||
organisationId,
|
||||
name,
|
||||
type: OrganisationGroupType.CUSTOM,
|
||||
organisationRole,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.organisationGroupMember.createMany({
|
||||
data: memberIds.map((memberId) => ({
|
||||
id: generateDatabaseId('group_member'),
|
||||
organisationMemberId: memberId,
|
||||
groupId: group.id,
|
||||
})),
|
||||
});
|
||||
|
||||
return group;
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import { OrganisationMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const createOrganisationGroupMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/organisation/{teamId}/groups',
|
||||
// summary: 'Create organisation group',
|
||||
// description: 'Create a new group for a organisation',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZCreateOrganisationGroupRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
organisationRole: z.nativeEnum(OrganisationMemberRole),
|
||||
name: z.string().max(100),
|
||||
memberIds: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const ZCreateOrganisationGroupResponseSchema = z.void();
|
||||
@ -0,0 +1,29 @@
|
||||
import { createOrganisationMemberInvites } from '@documenso/lib/server-only/organisation/create-organisation-member-invites';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZCreateOrganisationMemberInvitesRequestSchema,
|
||||
ZCreateOrganisationMemberInvitesResponseSchema,
|
||||
} from './create-organisation-member-invites.types';
|
||||
|
||||
export const createOrganisationMemberInvitesRoute = authenticatedProcedure
|
||||
.input(ZCreateOrganisationMemberInvitesRequestSchema)
|
||||
.output(ZCreateOrganisationMemberInvitesResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { organisationId, invitations } = input;
|
||||
const userId = ctx.user.id;
|
||||
const userName = ctx.user.name || '';
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
await createOrganisationMemberInvites({
|
||||
userId,
|
||||
userName,
|
||||
organisationId,
|
||||
invitations,
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,42 @@
|
||||
import { OrganisationMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const createOrganisationMemberInvitesMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/organisation/member/create',
|
||||
// summary: 'Invite organisation members',
|
||||
// description: 'Invite a users to be part of your organisation',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZCreateOrganisationMemberInvitesRequestSchema = z.object({
|
||||
organisationId: z.string().describe('The organisation to invite the user to'),
|
||||
invitations: z
|
||||
.array(
|
||||
z.object({
|
||||
email: z.string().trim().email().toLowerCase(),
|
||||
organisationRole: z.nativeEnum(OrganisationMemberRole),
|
||||
}),
|
||||
)
|
||||
.min(1)
|
||||
.refine(
|
||||
(invitations) => {
|
||||
const emails = invitations
|
||||
.filter((invitation) => invitation.email !== undefined)
|
||||
.map((invitation) => invitation.email);
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
{
|
||||
message: 'Emails must be unique, no duplicate values allowed',
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const ZCreateOrganisationMemberInvitesResponseSchema = z.void();
|
||||
|
||||
export type TCreateOrganisationMemberInvitesRequestSchema = z.infer<
|
||||
typeof ZCreateOrganisationMemberInvitesRequestSchema
|
||||
>;
|
||||
@ -0,0 +1,85 @@
|
||||
import { OrganisationType } from '@prisma/client';
|
||||
|
||||
import { createCheckoutSession } from '@documenso/ee/server-only/stripe/create-checkout-session';
|
||||
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
|
||||
import { IS_BILLING_ENABLED, NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { createOrganisation } from '@documenso/lib/server-only/organisation/create-organisation';
|
||||
import { INTERNAL_CLAIM_ID, internalClaims } from '@documenso/lib/types/subscription';
|
||||
import { generateStripeOrganisationCreateMetadata } from '@documenso/lib/utils/billing';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZCreateOrganisationRequestSchema,
|
||||
ZCreateOrganisationResponseSchema,
|
||||
} from './create-organisation.types';
|
||||
|
||||
export const createOrganisationRoute = authenticatedProcedure
|
||||
// .meta(createOrganisationMeta)
|
||||
.input(ZCreateOrganisationRequestSchema)
|
||||
.output(ZCreateOrganisationResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { name, priceId } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
priceId,
|
||||
},
|
||||
});
|
||||
|
||||
// Check if user can create a free organiastion.
|
||||
if (IS_BILLING_ENABLED() && !priceId) {
|
||||
const userOrganisations = await prisma.organisation.findMany({
|
||||
where: {
|
||||
ownerUserId: user.id,
|
||||
subscription: {
|
||||
is: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (userOrganisations.length >= 1) {
|
||||
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
|
||||
message: 'You have reached the maximum number of free organisations.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create checkout session for payment.
|
||||
if (IS_BILLING_ENABLED() && priceId) {
|
||||
const customer = await createCustomer({
|
||||
email: user.email,
|
||||
name: user.name || user.email,
|
||||
});
|
||||
|
||||
const checkoutUrl = await createCheckoutSession({
|
||||
priceId,
|
||||
customerId: customer.id,
|
||||
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/settings/organisations`,
|
||||
subscriptionMetadata: generateStripeOrganisationCreateMetadata(name, user.id),
|
||||
});
|
||||
|
||||
return {
|
||||
paymentRequired: true,
|
||||
checkoutUrl,
|
||||
};
|
||||
}
|
||||
|
||||
// Free organisations should be Personal by default.
|
||||
const organisationType = IS_BILLING_ENABLED()
|
||||
? OrganisationType.PERSONAL
|
||||
: OrganisationType.ORGANISATION;
|
||||
|
||||
await createOrganisation({
|
||||
userId: user.id,
|
||||
name,
|
||||
type: organisationType,
|
||||
claim: internalClaims[INTERNAL_CLAIM_ID.FREE],
|
||||
});
|
||||
|
||||
return {
|
||||
paymentRequired: false,
|
||||
};
|
||||
});
|
||||
@ -0,0 +1,33 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const createOrganisationMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/organisation',
|
||||
// summary: 'Create organisation',
|
||||
// description: 'Create an organisation',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZOrganisationNameSchema = z
|
||||
.string()
|
||||
.min(3, { message: 'Minimum 3 characters' })
|
||||
.max(50, { message: 'Maximum 50 characters' });
|
||||
|
||||
export const ZCreateOrganisationRequestSchema = z.object({
|
||||
name: ZOrganisationNameSchema,
|
||||
priceId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ZCreateOrganisationResponseSchema = z.union([
|
||||
z.object({
|
||||
paymentRequired: z.literal(false),
|
||||
}),
|
||||
z.object({
|
||||
paymentRequired: z.literal(true),
|
||||
checkoutUrl: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type TCreateOrganisationResponse = z.infer<typeof ZCreateOrganisationResponseSchema>;
|
||||
@ -0,0 +1,38 @@
|
||||
import { OrganisationMemberInviteStatus } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { maybeAuthenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZDeclineOrganisationMemberInviteRequestSchema,
|
||||
ZDeclineOrganisationMemberInviteResponseSchema,
|
||||
} from './decline-organisation-member-invite.types';
|
||||
|
||||
export const declineOrganisationMemberInviteRoute = maybeAuthenticatedProcedure
|
||||
.input(ZDeclineOrganisationMemberInviteRequestSchema)
|
||||
.output(ZDeclineOrganisationMemberInviteResponseSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const { token } = input;
|
||||
|
||||
const organisationMemberInvite = await prisma.organisationMemberInvite.findFirst({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisationMemberInvite) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
await prisma.organisationMemberInvite.update({
|
||||
where: {
|
||||
id: organisationMemberInvite.id,
|
||||
},
|
||||
data: {
|
||||
status: OrganisationMemberInviteStatus.DECLINED,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: notify the team owner
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZDeclineOrganisationMemberInviteRequestSchema = z.object({
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
export const ZDeclineOrganisationMemberInviteResponseSchema = z.void();
|
||||
|
||||
export type TDeclineOrganisationMemberInviteResponse = z.infer<
|
||||
typeof ZDeclineOrganisationMemberInviteResponseSchema
|
||||
>;
|
||||
@ -0,0 +1,69 @@
|
||||
import { OrganisationGroupType } from '@prisma/client';
|
||||
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZDeleteOrganisationGroupRequestSchema,
|
||||
ZDeleteOrganisationGroupResponseSchema,
|
||||
} from './delete-organisation-group.types';
|
||||
|
||||
export const deleteOrganisationGroupRoute = authenticatedProcedure
|
||||
// .meta(deleteOrganisationGroupMeta)
|
||||
.input(ZDeleteOrganisationGroupRequestSchema)
|
||||
.output(ZDeleteOrganisationGroupResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { groupId, organisationId } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
groupId,
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId: user.id,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const group = await prisma.organisationGroup.findFirst({
|
||||
where: {
|
||||
id: groupId,
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Organisation group not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
group.type === OrganisationGroupType.INTERNAL_ORGANISATION ||
|
||||
group.type === OrganisationGroupType.INTERNAL_TEAM
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You are not allowed to delete internal groups',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.organisationGroup.delete({
|
||||
where: {
|
||||
id: groupId,
|
||||
organisationId: organisation.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const deleteOrganisationGroupMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/organisation/groups/{id}/delete',
|
||||
// summary: 'Delete organisation group',
|
||||
// description: 'Delete an existing group for a organisation',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZDeleteOrganisationGroupRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
groupId: z.string(),
|
||||
});
|
||||
|
||||
export const ZDeleteOrganisationGroupResponseSchema = z.void();
|
||||
@ -0,0 +1,79 @@
|
||||
import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { validateIfSubscriptionIsRequired } from '@documenso/lib/utils/billing';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZDeleteOrganisationMemberInvitesRequestSchema,
|
||||
ZDeleteOrganisationMemberInvitesResponseSchema,
|
||||
} from './delete-organisation-member-invites.types';
|
||||
|
||||
export const deleteOrganisationMemberInvitesRoute = authenticatedProcedure
|
||||
// .meta(deleteOrganisationMemberInvitesMeta)
|
||||
.input(ZDeleteOrganisationMemberInvitesRequestSchema)
|
||||
.output(ZDeleteOrganisationMemberInvitesResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { organisationId, invitationIds } = input;
|
||||
const userId = ctx.user.id;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
invitationIds,
|
||||
},
|
||||
});
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
include: {
|
||||
organisationClaim: true,
|
||||
subscription: true,
|
||||
members: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
invites: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const { organisationClaim } = organisation;
|
||||
|
||||
const subscription = validateIfSubscriptionIsRequired(organisation.subscription);
|
||||
|
||||
const numberOfCurrentMembers = organisation.members.length;
|
||||
const numberOfCurrentInvites = organisation.invites.length;
|
||||
const totalMemberCountWithInvites = numberOfCurrentMembers + numberOfCurrentInvites - 1;
|
||||
|
||||
if (subscription) {
|
||||
await syncMemberCountWithStripeSeatPlan(
|
||||
subscription,
|
||||
organisationClaim,
|
||||
totalMemberCountWithInvites,
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.organisationMemberInvite.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: invitationIds,
|
||||
},
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const deleteOrganisationMemberInvitesMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/organisation/member/delete-many',
|
||||
// summary: 'Delete organisation member invites',
|
||||
// description: 'Delete organisation member invites',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZDeleteOrganisationMemberInvitesRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
invitationIds: z.array(z.string()).refine((items) => new Set(items).size === items.length, {
|
||||
message: 'Invitation IDs must be unique, no duplicate values allowed',
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZDeleteOrganisationMemberInvitesResponseSchema = z.void();
|
||||
@ -0,0 +1,28 @@
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZDeleteOrganisationMemberRequestSchema,
|
||||
ZDeleteOrganisationMemberResponseSchema,
|
||||
} from './delete-organisation-member.types';
|
||||
import { deleteOrganisationMembers } from './delete-organisation-members';
|
||||
|
||||
export const deleteOrganisationMemberRoute = authenticatedProcedure
|
||||
// .meta(deleteOrganisationMemberMeta)
|
||||
.input(ZDeleteOrganisationMemberRequestSchema)
|
||||
.output(ZDeleteOrganisationMemberResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { organisationId, organisationMemberId } = input;
|
||||
const userId = ctx.user.id;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
organisationMemberId,
|
||||
},
|
||||
});
|
||||
|
||||
await deleteOrganisationMembers({
|
||||
userId,
|
||||
organisationId,
|
||||
organisationMemberIds: [organisationMemberId],
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const deleteOrganisationMemberMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/organisation/member/delete',
|
||||
// summary: 'Delete organisation member',
|
||||
// description: 'Delete organisation member',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZDeleteOrganisationMemberRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
organisationMemberId: z.string(),
|
||||
});
|
||||
|
||||
export const ZDeleteOrganisationMemberResponseSchema = z.void();
|
||||
@ -0,0 +1,112 @@
|
||||
import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { jobs } from '@documenso/lib/jobs/client';
|
||||
import { validateIfSubscriptionIsRequired } from '@documenso/lib/utils/billing';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { OrganisationMemberInviteStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZDeleteOrganisationMembersRequestSchema,
|
||||
ZDeleteOrganisationMembersResponseSchema,
|
||||
} from './delete-organisation-members.types';
|
||||
|
||||
export const deleteOrganisationMembersRoute = authenticatedProcedure
|
||||
// .meta(deleteOrganisationMembersMeta)
|
||||
.input(ZDeleteOrganisationMembersRequestSchema)
|
||||
.output(ZDeleteOrganisationMembersResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { organisationId, organisationMemberIds } = input;
|
||||
const userId = ctx.user.id;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
organisationMemberIds,
|
||||
},
|
||||
});
|
||||
|
||||
await deleteOrganisationMembers({
|
||||
userId,
|
||||
organisationId,
|
||||
organisationMemberIds,
|
||||
});
|
||||
});
|
||||
|
||||
type DeleteOrganisationMembersProps = {
|
||||
userId: number;
|
||||
organisationId: string;
|
||||
organisationMemberIds: string[];
|
||||
};
|
||||
|
||||
export const deleteOrganisationMembers = async ({
|
||||
userId,
|
||||
organisationId,
|
||||
organisationMemberIds,
|
||||
}: DeleteOrganisationMembersProps) => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
include: {
|
||||
subscription: true,
|
||||
organisationClaim: true,
|
||||
members: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
invites: {
|
||||
where: {
|
||||
status: OrganisationMemberInviteStatus.PENDING,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const { organisationClaim } = organisation;
|
||||
|
||||
const membersToDelete = organisation.members.filter((member) =>
|
||||
organisationMemberIds.includes(member.id),
|
||||
);
|
||||
|
||||
const subscription = validateIfSubscriptionIsRequired(organisation.subscription);
|
||||
|
||||
const inviteCount = organisation.invites.length;
|
||||
const newMemberCount = organisation.members.length + inviteCount - membersToDelete.length;
|
||||
|
||||
if (subscription) {
|
||||
await syncMemberCountWithStripeSeatPlan(subscription, organisationClaim, newMemberCount);
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.organisationMember.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: organisationMemberIds,
|
||||
},
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
await jobs.triggerJob({
|
||||
name: 'send.organisation-member-left.email',
|
||||
payload: {
|
||||
organisationId,
|
||||
memberUserId: userId,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,22 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const deleteOrganisationMembersMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/organisation/member/delete-many',
|
||||
// summary: 'Delete organisation members',
|
||||
// description: 'Delete organisation members',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZDeleteOrganisationMembersRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
organisationMemberIds: z
|
||||
.array(z.string())
|
||||
.refine((items) => new Set(items).size === items.length, {
|
||||
message: 'Organisation member ids must be unique, no duplicate values allowed',
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZDeleteOrganisationMembersResponseSchema = z.void();
|
||||
@ -0,0 +1,45 @@
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZDeleteOrganisationRequestSchema,
|
||||
ZDeleteOrganisationResponseSchema,
|
||||
} from './delete-organisation.types';
|
||||
|
||||
export const deleteOrganisationRoute = authenticatedProcedure
|
||||
// .meta(deleteOrganisationMeta)
|
||||
.input(ZDeleteOrganisationRequestSchema)
|
||||
.output(ZDeleteOrganisationResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { organisationId } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId: user.id,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['DELETE_ORGANISATION'],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You are not authorized to delete this organisation',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.organisation.delete({
|
||||
where: {
|
||||
id: organisation.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const deleteOrganisationMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'DELETE',
|
||||
// path: '/organisation/{teamId}',
|
||||
// summary: 'Delete organisation',
|
||||
// description: 'Delete an existing organisation',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZDeleteOrganisationRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
});
|
||||
|
||||
export const ZDeleteOrganisationResponseSchema = z.void();
|
||||
@ -0,0 +1,170 @@
|
||||
import type { OrganisationGroupType, OrganisationMemberRole } from '@prisma/client';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import type { FindResultResponse } from '@documenso/lib/types/search-params';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZFindOrganisationGroupsRequestSchema,
|
||||
ZFindOrganisationGroupsResponseSchema,
|
||||
} from './find-organisation-groups.types';
|
||||
|
||||
export const findOrganisationGroupsRoute = authenticatedProcedure
|
||||
// .meta(findOrganisationGroupsMeta)
|
||||
.input(ZFindOrganisationGroupsRequestSchema)
|
||||
.output(ZFindOrganisationGroupsResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { organisationId, types, query, page, perPage, organisationGroupId, organisationRoles } =
|
||||
input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
},
|
||||
});
|
||||
|
||||
return await findOrganisationGroups({
|
||||
userId: user.id,
|
||||
organisationId,
|
||||
organisationGroupId,
|
||||
organisationRoles,
|
||||
types,
|
||||
query,
|
||||
page,
|
||||
perPage,
|
||||
});
|
||||
});
|
||||
|
||||
type FindOrganisationGroupsOptions = {
|
||||
userId: number;
|
||||
organisationId: string;
|
||||
organisationGroupId?: string;
|
||||
organisationRoles?: OrganisationMemberRole[];
|
||||
types?: OrganisationGroupType[];
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
};
|
||||
|
||||
export const findOrganisationGroups = async ({
|
||||
userId,
|
||||
organisationId,
|
||||
organisationGroupId,
|
||||
organisationRoles = [],
|
||||
types = [],
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
}: FindOrganisationGroupsOptions) => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({ organisationId, userId }),
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const whereClause: Prisma.OrganisationGroupWhereInput = {
|
||||
organisationId: organisation.id,
|
||||
type:
|
||||
types.length > 0
|
||||
? {
|
||||
in: types,
|
||||
}
|
||||
: undefined,
|
||||
organisationRole:
|
||||
organisationRoles.length > 0
|
||||
? {
|
||||
in: organisationRoles,
|
||||
}
|
||||
: undefined,
|
||||
id: organisationGroupId,
|
||||
};
|
||||
|
||||
if (query) {
|
||||
whereClause.name = {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
};
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.organisationGroup.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
name: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
type: true,
|
||||
organisationId: true,
|
||||
organisationRole: true,
|
||||
teamGroups: {
|
||||
select: {
|
||||
id: true,
|
||||
teamId: true,
|
||||
teamRole: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
organisationGroupMembers: {
|
||||
select: {
|
||||
organisationMember: {
|
||||
select: {
|
||||
id: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
avatarImageId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.organisationGroup.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
const mappedData = data.map((group) => ({
|
||||
...group,
|
||||
teams: group.teamGroups.map((teamGroup) => ({
|
||||
id: teamGroup.team.id,
|
||||
name: teamGroup.team.name,
|
||||
teamGroupId: teamGroup.id,
|
||||
teamRole: teamGroup.teamRole,
|
||||
})),
|
||||
members: group.organisationGroupMembers.map(({ organisationMember }) => ({
|
||||
id: organisationMember.id,
|
||||
userId: organisationMember.user.id,
|
||||
name: organisationMember.user.name || '',
|
||||
email: organisationMember.user.email,
|
||||
avatarImageId: organisationMember.user.avatarImageId,
|
||||
})),
|
||||
}));
|
||||
|
||||
return {
|
||||
data: mappedData,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof mappedData>;
|
||||
};
|
||||
@ -0,0 +1,54 @@
|
||||
import { OrganisationGroupType, OrganisationMemberRole, TeamMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { OrganisationGroupSchema } from '@documenso/prisma/generated/zod/modelSchema/OrganisationGroupSchema';
|
||||
|
||||
// export const getOrganisationGroupsMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'GET',
|
||||
// path: '/organisation/{teamId}/groups',
|
||||
// summary: 'Get organisation groups',
|
||||
// description: 'Get all groups for a organisation',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZFindOrganisationGroupsRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
organisationId: z.string(),
|
||||
organisationGroupId: z.string().optional(),
|
||||
organisationRoles: z.nativeEnum(OrganisationMemberRole).array().optional(),
|
||||
types: z.nativeEnum(OrganisationGroupType).array().optional(),
|
||||
});
|
||||
|
||||
export const ZFindOrganisationGroupsResponseSchema = ZFindResultResponse.extend({
|
||||
data: OrganisationGroupSchema.pick({
|
||||
type: true,
|
||||
organisationRole: true,
|
||||
id: true,
|
||||
name: true,
|
||||
organisationId: true,
|
||||
})
|
||||
.extend({
|
||||
members: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
userId: z.number(),
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
avatarImageId: z.string().nullable(),
|
||||
})
|
||||
.array(),
|
||||
teams: z
|
||||
.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
teamGroupId: z.string(),
|
||||
teamRole: z.nativeEnum(TeamMemberRole),
|
||||
})
|
||||
.array(),
|
||||
})
|
||||
.array(),
|
||||
});
|
||||
|
||||
export type TFindOrganisationGroupsResponse = z.infer<typeof ZFindOrganisationGroupsResponseSchema>;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user