mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 09:12:02 +10:00
fix: merge conflicts
This commit is contained in:
@ -0,0 +1,28 @@
|
||||
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 }) => {
|
||||
const { ownerUserId, data } = input;
|
||||
|
||||
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>;
|
||||
50
packages/trpc/server/admin-router/create-stripe-customer.ts
Normal file
50
packages/trpc/server/admin-router/create-stripe-customer.ts
Normal file
@ -0,0 +1,50 @@
|
||||
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 }) => {
|
||||
const { organisationId } = input;
|
||||
|
||||
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,23 @@
|
||||
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 }) => {
|
||||
const { name, teamCount, memberCount, flags } = 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,37 @@
|
||||
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 }) => {
|
||||
const { id } = input;
|
||||
|
||||
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>;
|
||||
56
packages/trpc/server/admin-router/get-admin-organisation.ts
Normal file
56
packages/trpc/server/admin-router/get-admin-organisation.ts
Normal file
@ -0,0 +1,56 @@
|
||||
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 }) => {
|
||||
const { organisationId } = input;
|
||||
|
||||
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>;
|
||||
@ -14,6 +14,13 @@ 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 {
|
||||
ZAdminDeleteDocumentMutationSchema,
|
||||
ZAdminDeleteUserMutationSchema,
|
||||
@ -25,8 +32,27 @@ 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,
|
||||
},
|
||||
|
||||
// Todo: migrate old routes
|
||||
findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => {
|
||||
const { query, page, perPage } = input;
|
||||
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
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 }) => {
|
||||
const { organisationId, data } = input;
|
||||
|
||||
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,65 @@
|
||||
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 }) => {
|
||||
const { id, data } = 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,31 +1,15 @@
|
||||
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 }) => {
|
||||
|
||||
@ -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>;
|
||||
|
||||
81
packages/trpc/server/billing/create-subscription.ts
Normal file
81
packages/trpc/server/billing/create-subscription.ts
Normal file
@ -0,0 +1,81 @@
|
||||
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 } = input;
|
||||
|
||||
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.name,
|
||||
email: organisation.owner.email,
|
||||
});
|
||||
|
||||
customerId = customer.id;
|
||||
|
||||
await prisma.organisation.update({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
data: {
|
||||
customerId: customer.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const redirectUrl = await createCheckoutSession({
|
||||
customerId,
|
||||
priceId,
|
||||
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/o/${organisation.url}/settings/billing`,
|
||||
});
|
||||
|
||||
if (!redirectUrl) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Failed to create checkout session',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
redirectUrl,
|
||||
};
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
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'),
|
||||
});
|
||||
58
packages/trpc/server/billing/get-invoices.ts
Normal file
58
packages/trpc/server/billing/get-invoices.ts
Normal file
@ -0,0 +1,58 @@
|
||||
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;
|
||||
|
||||
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,
|
||||
}));
|
||||
});
|
||||
5
packages/trpc/server/billing/get-invoices.types.ts
Normal file
5
packages/trpc/server/billing/get-invoices.types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZGetInvoicesRequestSchema = z.object({
|
||||
organisationId: z.string().describe('The organisation to get the invoices for'),
|
||||
});
|
||||
31
packages/trpc/server/billing/get-plans.ts
Normal file
31
packages/trpc/server/billing/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,
|
||||
};
|
||||
});
|
||||
34
packages/trpc/server/billing/get-subscription.ts
Normal file
34
packages/trpc/server/billing/get-subscription.ts
Normal file
@ -0,0 +1,34 @@
|
||||
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;
|
||||
|
||||
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,
|
||||
};
|
||||
});
|
||||
5
packages/trpc/server/billing/get-subscription.types.ts
Normal file
5
packages/trpc/server/billing/get-subscription.types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZGetSubscriptionRequestSchema = z.object({
|
||||
organisationId: z.string().describe('The organisation to get the subscription for'),
|
||||
});
|
||||
98
packages/trpc/server/billing/manage-subscription.ts
Normal file
98
packages/trpc/server/billing/manage-subscription.ts
Normal file
@ -0,0 +1,98 @@
|
||||
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;
|
||||
|
||||
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.name,
|
||||
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'),
|
||||
});
|
||||
20
packages/trpc/server/billing/router.ts
Normal file
20
packages/trpc/server/billing/router.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { router } from '../trpc';
|
||||
import { createSubscriptionRoute } from './create-subscription';
|
||||
import { getInvoicesRoute } from './get-invoices';
|
||||
import { getPlansRoute } from './get-plans';
|
||||
import { getSubscriptionRoute } from './get-subscription';
|
||||
import { manageSubscriptionRoute } from './manage-subscription';
|
||||
|
||||
export const billingRouter = router({
|
||||
plans: {
|
||||
get: getPlansRoute,
|
||||
},
|
||||
subscription: {
|
||||
get: getSubscriptionRoute,
|
||||
create: createSubscriptionRoute,
|
||||
manage: manageSubscriptionRoute,
|
||||
},
|
||||
invoices: {
|
||||
get: getInvoicesRoute,
|
||||
},
|
||||
});
|
||||
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>;
|
||||
@ -19,7 +19,6 @@ 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';
|
||||
@ -28,6 +27,8 @@ import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-action
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
|
||||
import { authenticatedProcedure, procedure, router } from '../trpc';
|
||||
import { findInboxRoute } from './find-inbox';
|
||||
import { getInboxCountRoute } from './get-inbox-count';
|
||||
import {
|
||||
ZCreateDocumentRequestSchema,
|
||||
ZCreateDocumentV2RequestSchema,
|
||||
@ -49,8 +50,6 @@ import {
|
||||
ZGetDocumentByTokenQuerySchema,
|
||||
ZGetDocumentWithDetailsByIdRequestSchema,
|
||||
ZGetDocumentWithDetailsByIdResponseSchema,
|
||||
ZMoveDocumentToTeamResponseSchema,
|
||||
ZMoveDocumentToTeamSchema,
|
||||
ZResendDocumentMutationSchema,
|
||||
ZSearchDocumentsMutationSchema,
|
||||
ZSetSigningOrderForDocumentMutationSchema,
|
||||
@ -59,6 +58,11 @@ import {
|
||||
import { updateDocumentRoute } from './update-document';
|
||||
|
||||
export const documentRouter = router({
|
||||
inbox: {
|
||||
find: findInboxRoute,
|
||||
getCount: getInboxCountRoute,
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@ -174,7 +178,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,
|
||||
};
|
||||
@ -255,7 +259,7 @@ export const documentRouter = router({
|
||||
.input(ZCreateDocumentV2RequestSchema)
|
||||
.output(ZCreateDocumentV2ResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx;
|
||||
const { teamId, user } = ctx;
|
||||
|
||||
const {
|
||||
title,
|
||||
@ -267,7 +271,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, {
|
||||
@ -325,10 +329,10 @@ export const documentRouter = router({
|
||||
// })
|
||||
.input(ZCreateDocumentRequestSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx;
|
||||
const { user, teamId } = ctx;
|
||||
const { title, documentDataId, timezone, folderId } = 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, {
|
||||
@ -338,7 +342,7 @@ export const documentRouter = router({
|
||||
}
|
||||
|
||||
return await createDocument({
|
||||
userId: ctx.user.id,
|
||||
userId: user.id,
|
||||
teamId,
|
||||
title,
|
||||
documentDataId,
|
||||
@ -381,33 +385,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
|
||||
*
|
||||
|
||||
@ -220,8 +220,8 @@ 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(
|
||||
@ -358,10 +358,3 @@ export const ZDownloadAuditLogsMutationSchema = z.object({
|
||||
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 ZMoveDocumentToTeamResponseSchema = ZDocumentLiteSchema;
|
||||
|
||||
@ -42,8 +42,8 @@ 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(),
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { isCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan';
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
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 { prisma } from '@documenso/prisma';
|
||||
|
||||
import { procedure } from '../trpc';
|
||||
import {
|
||||
@ -42,24 +40,11 @@ export const createEmbeddingPresignTokenRoute = procedure
|
||||
});
|
||||
}
|
||||
|
||||
const [hasCommunityPlan, hasEnterprisePlan] = await Promise.all([
|
||||
isCommunityPlan({ userId: token.userId, teamId: token.teamId ?? undefined }),
|
||||
isUserEnterprise({ userId: token.userId, teamId: token.teamId ?? undefined }),
|
||||
]);
|
||||
const organisationClaim = await getOrganisationClaimByTeamId({
|
||||
teamId: token.teamId,
|
||||
});
|
||||
|
||||
let hasTeamAuthoringFlag = false;
|
||||
|
||||
if (token.teamId) {
|
||||
const teamGlobalSettings = await prisma.teamGlobalSettings.findFirst({
|
||||
where: {
|
||||
teamId: token.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
hasTeamAuthoringFlag = teamGlobalSettings?.allowEmbeddedAuthoring ?? false;
|
||||
}
|
||||
|
||||
if (!hasCommunityPlan && !hasEnterprisePlan && !hasTeamAuthoringFlag) {
|
||||
if (!organisationClaim.flags.embedAuthoring) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to create embedding presign tokens',
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
@ -111,13 +112,6 @@ export const ZGetFoldersResponseSchema = z.object({
|
||||
|
||||
export type TGetFoldersResponse = z.infer<typeof ZGetFoldersResponseSchema>;
|
||||
|
||||
export const ZFindSearchParamsSchema = z.object({
|
||||
query: z.string().optional(),
|
||||
page: z.number().optional(),
|
||||
perPage: z.number().optional(),
|
||||
type: ZFolderTypeSchema.optional(),
|
||||
});
|
||||
|
||||
export const ZFindFoldersRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
parentId: z.string().nullable().optional(),
|
||||
type: ZFolderTypeSchema.optional(),
|
||||
|
||||
@ -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,91 @@
|
||||
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;
|
||||
|
||||
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,23 @@
|
||||
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 || '';
|
||||
|
||||
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,79 @@
|
||||
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;
|
||||
|
||||
// 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,62 @@
|
||||
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;
|
||||
|
||||
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,72 @@
|
||||
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;
|
||||
|
||||
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,21 @@
|
||||
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;
|
||||
|
||||
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,105 @@
|
||||
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;
|
||||
|
||||
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,39 @@
|
||||
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;
|
||||
|
||||
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,164 @@
|
||||
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;
|
||||
|
||||
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>;
|
||||
@ -0,0 +1,105 @@
|
||||
import type { OrganisationMemberInviteStatus } from '@prisma/client';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
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 {
|
||||
ZFindOrganisationMemberInvitesRequestSchema,
|
||||
ZFindOrganisationMemberInvitesResponseSchema,
|
||||
} from './find-organisation-member-invites.types';
|
||||
|
||||
export const findOrganisationMemberInvitesRoute = authenticatedProcedure
|
||||
// .meta(getOrganisationMemberInvitesMeta)
|
||||
.input(ZFindOrganisationMemberInvitesRequestSchema)
|
||||
.output(ZFindOrganisationMemberInvitesResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { organisationId, query, page, perPage, status } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
return await findOrganisationMemberInvites({
|
||||
userId: user.id,
|
||||
organisationId,
|
||||
query,
|
||||
page,
|
||||
perPage,
|
||||
status,
|
||||
});
|
||||
});
|
||||
|
||||
type FindOrganisationMemberInvitesOptions = {
|
||||
userId: number;
|
||||
organisationId: string;
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
status?: OrganisationMemberInviteStatus;
|
||||
};
|
||||
|
||||
export const findOrganisationMemberInvites = async ({
|
||||
userId,
|
||||
organisationId,
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
status,
|
||||
}: FindOrganisationMemberInvitesOptions) => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const whereClause: Prisma.OrganisationMemberInviteWhereInput = {
|
||||
organisationId: organisation.id,
|
||||
status,
|
||||
};
|
||||
|
||||
if (query) {
|
||||
whereClause.email = {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
};
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.organisationMemberInvite.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
// Exclude token attribute.
|
||||
select: {
|
||||
id: true,
|
||||
organisationId: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
organisationRole: true,
|
||||
status: true,
|
||||
},
|
||||
}),
|
||||
prisma.organisationMemberInvite.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
import { OrganisationMemberInviteStatus } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import { OrganisationMemberInviteSchema } from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberInviteSchema';
|
||||
|
||||
// export const getOrganisationMemberInvitesMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'GET',
|
||||
// path: '/organisation/{teamId}/members/pending',
|
||||
// summary: 'Find organisation members pending',
|
||||
// description: 'Find all members of a organisation pending',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZFindOrganisationMemberInvitesRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
organisationId: z.string(),
|
||||
status: z.nativeEnum(OrganisationMemberInviteStatus).optional(),
|
||||
});
|
||||
|
||||
export const ZFindOrganisationMemberInvitesResponseSchema = ZFindResultResponse.extend({
|
||||
data: OrganisationMemberInviteSchema.pick({
|
||||
id: true,
|
||||
organisationId: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
organisationRole: true,
|
||||
status: true,
|
||||
}).array(),
|
||||
});
|
||||
|
||||
export type TFindOrganisationMemberInvitesResponse = z.infer<
|
||||
typeof ZFindOrganisationMemberInvitesResponseSchema
|
||||
>;
|
||||
@ -0,0 +1,137 @@
|
||||
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,
|
||||
getHighestOrganisationRoleInGroup,
|
||||
} from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZFindOrganisationMembersRequestSchema,
|
||||
ZFindOrganisationMembersResponseSchema,
|
||||
} from './find-organisation-members.types';
|
||||
|
||||
export const findOrganisationMembersRoute = authenticatedProcedure
|
||||
// .meta(getOrganisationMembersMeta)
|
||||
.input(ZFindOrganisationMembersRequestSchema)
|
||||
.output(ZFindOrganisationMembersResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { organisationId } = input;
|
||||
const { id } = ctx.user;
|
||||
|
||||
const organisationMembers = await findOrganisationMembers({
|
||||
userId: id,
|
||||
organisationId,
|
||||
query: input.query,
|
||||
page: input.page,
|
||||
perPage: input.perPage,
|
||||
});
|
||||
|
||||
return {
|
||||
...organisationMembers,
|
||||
data: organisationMembers.data.map((organisationMember) => {
|
||||
const groups = organisationMember.organisationGroupMembers.map((group) => group.group);
|
||||
|
||||
return {
|
||||
id: organisationMember.id,
|
||||
userId: organisationMember.user.id,
|
||||
email: organisationMember.user.email,
|
||||
name: organisationMember.user.name || '',
|
||||
createdAt: organisationMember.createdAt,
|
||||
currentOrganisationRole: getHighestOrganisationRoleInGroup(groups),
|
||||
avatarImageId: organisationMember.user.avatarImageId,
|
||||
groups,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
type FindOrganisationMembersOptions = {
|
||||
userId: number;
|
||||
organisationId: string;
|
||||
query?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
};
|
||||
|
||||
export const findOrganisationMembers = async ({
|
||||
userId,
|
||||
organisationId,
|
||||
query,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
}: FindOrganisationMembersOptions) => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({ organisationId, userId }),
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const whereClause: Prisma.OrganisationMemberWhereInput = {
|
||||
organisationId: organisation.id,
|
||||
};
|
||||
|
||||
if (query) {
|
||||
whereClause.user = {
|
||||
OR: [
|
||||
{
|
||||
email: {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: {
|
||||
contains: query,
|
||||
mode: Prisma.QueryMode.insensitive,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.organisationMember.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
organisationId: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
avatarImageId: true,
|
||||
},
|
||||
},
|
||||
organisationGroupMembers: {
|
||||
select: {
|
||||
group: true,
|
||||
},
|
||||
},
|
||||
createdAt: true,
|
||||
},
|
||||
}),
|
||||
prisma.organisationMember.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof data>;
|
||||
};
|
||||
@ -0,0 +1,46 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import OrganisationMemberRoleSchema from '@documenso/prisma/generated/zod/inputTypeSchemas/OrganisationMemberRoleSchema';
|
||||
import OrganisationGroupSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationGroupSchema';
|
||||
import { OrganisationMemberSchema } from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberSchema';
|
||||
|
||||
// export const getOrganisationMembersMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'GET',
|
||||
// path: '/organisation/{teamId}/members',
|
||||
// summary: 'Find organisation members',
|
||||
// description: 'Find all members of a organisation',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZFindOrganisationMembersRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
organisationId: z.string(),
|
||||
});
|
||||
|
||||
export const ZFindOrganisationMembersResponseSchema = ZFindResultResponse.extend({
|
||||
data: OrganisationMemberSchema.pick({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
userId: true,
|
||||
})
|
||||
.extend({
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
avatarImageId: z.string().nullable(),
|
||||
currentOrganisationRole: OrganisationMemberRoleSchema,
|
||||
groups: z.array(
|
||||
OrganisationGroupSchema.pick({
|
||||
id: true,
|
||||
organisationRole: true,
|
||||
type: true,
|
||||
}),
|
||||
),
|
||||
})
|
||||
.array(),
|
||||
});
|
||||
|
||||
export type TFindOrganisationMembersResponse = z.infer<
|
||||
typeof ZFindOrganisationMembersResponseSchema
|
||||
>;
|
||||
@ -0,0 +1,34 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetOrganisationMemberInvitesRequestSchema,
|
||||
ZGetOrganisationMemberInvitesResponseSchema,
|
||||
} from './get-organisation-member-invites.types';
|
||||
|
||||
export const getOrganisationMemberInvitesRoute = authenticatedProcedure
|
||||
// .meta(getOrganisationMemberInvitesMeta)
|
||||
.input(ZGetOrganisationMemberInvitesRequestSchema)
|
||||
.output(ZGetOrganisationMemberInvitesResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { user } = ctx;
|
||||
|
||||
const { status } = input;
|
||||
|
||||
return await prisma.organisationMemberInvite.findMany({
|
||||
where: {
|
||||
email: user.email,
|
||||
status,
|
||||
},
|
||||
include: {
|
||||
organisation: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
avatarImageId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,30 @@
|
||||
import { OrganisationMemberInviteStatus } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { OrganisationMemberInviteSchema } from '@documenso/prisma/generated/zod/modelSchema/OrganisationMemberInviteSchema';
|
||||
import OrganisationSchema from '@documenso/prisma/generated/zod/modelSchema/OrganisationSchema';
|
||||
|
||||
export const ZGetOrganisationMemberInvitesRequestSchema = z.object({
|
||||
status: z.nativeEnum(OrganisationMemberInviteStatus).optional(),
|
||||
});
|
||||
|
||||
export const ZGetOrganisationMemberInvitesResponseSchema = OrganisationMemberInviteSchema.pick({
|
||||
id: true,
|
||||
organisationId: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
token: true,
|
||||
})
|
||||
.extend({
|
||||
organisation: OrganisationSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
avatarImageId: true,
|
||||
}),
|
||||
})
|
||||
.array();
|
||||
|
||||
export type TGetOrganisationMemberInvitesResponse = z.infer<
|
||||
typeof ZGetOrganisationMemberInvitesResponseSchema
|
||||
>;
|
||||
@ -0,0 +1,79 @@
|
||||
import { getHighestOrganisationRoleInGroup } from '@documenso/lib/utils/organisations';
|
||||
import { buildTeamWhereQuery, getHighestTeamRoleInGroup } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import type { TGetOrganisationSessionResponse } from './get-organisation-session.types';
|
||||
import { ZGetOrganisationSessionResponseSchema } from './get-organisation-session.types';
|
||||
|
||||
/**
|
||||
* Get all the organisations and teams a user belongs to.
|
||||
*/
|
||||
export const getOrganisationSessionRoute = authenticatedProcedure
|
||||
.output(ZGetOrganisationSessionResponseSchema)
|
||||
.query(async ({ ctx }) => {
|
||||
return await getOrganisationSession({ userId: ctx.user.id });
|
||||
});
|
||||
|
||||
export const getOrganisationSession = async ({
|
||||
userId,
|
||||
}: {
|
||||
userId: number;
|
||||
}): Promise<TGetOrganisationSessionResponse> => {
|
||||
const organisations = await prisma.organisation.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organisationClaim: true,
|
||||
subscription: true,
|
||||
groups: {
|
||||
where: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
where: buildTeamWhereQuery({ teamId: undefined, userId }),
|
||||
include: {
|
||||
teamGroups: {
|
||||
where: {
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organisationGroup: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return organisations.map((organisation) => {
|
||||
return {
|
||||
...organisation,
|
||||
teams: organisation.teams.map((team) => ({
|
||||
...team,
|
||||
currentTeamRole: getHighestTeamRoleInGroup(team.teamGroups),
|
||||
})),
|
||||
currentOrganisationRole: getHighestOrganisationRoleInGroup(organisation.groups),
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZOrganisationSchema } from '@documenso/lib/types/organisation';
|
||||
import { OrganisationMemberRole, TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
import SubscriptionSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
|
||||
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
|
||||
export const ZGetOrganisationSessionResponseSchema = ZOrganisationSchema.extend({
|
||||
teams: z.array(
|
||||
TeamSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
createdAt: true,
|
||||
avatarImageId: true,
|
||||
organisationId: true,
|
||||
}).extend({
|
||||
currentTeamRole: z.nativeEnum(TeamMemberRole),
|
||||
}),
|
||||
),
|
||||
subscription: SubscriptionSchema.nullable(),
|
||||
currentOrganisationRole: z.nativeEnum(OrganisationMemberRole),
|
||||
}).array();
|
||||
|
||||
export type TGetOrganisationSessionResponse = z.infer<typeof ZGetOrganisationSessionResponseSchema>;
|
||||
|
||||
export type TeamSession = TGetOrganisationSessionResponse[number]['teams'][number];
|
||||
export type OrganisationSession = TGetOrganisationSessionResponse[number];
|
||||
79
packages/trpc/server/organisation-router/get-organisation.ts
Normal file
79
packages/trpc/server/organisation-router/get-organisation.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetOrganisationRequestSchema,
|
||||
ZGetOrganisationResponseSchema,
|
||||
} from './get-organisation.types';
|
||||
|
||||
export const getOrganisationRoute = authenticatedProcedure
|
||||
// .meta(getOrganisationMeta)
|
||||
.input(ZGetOrganisationRequestSchema)
|
||||
.output(ZGetOrganisationResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { organisationReference } = input;
|
||||
|
||||
return await getOrganisation({
|
||||
userId: ctx.user.id,
|
||||
organisationReference,
|
||||
});
|
||||
});
|
||||
|
||||
type GetOrganisationOptions = {
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* The ID or URL of the organisation.
|
||||
*/
|
||||
organisationReference: string;
|
||||
};
|
||||
|
||||
export const getOrganisation = async ({
|
||||
userId,
|
||||
organisationReference,
|
||||
}: GetOrganisationOptions) => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: {
|
||||
OR: [{ id: organisationReference }, { url: organisationReference }],
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organisationGlobalSettings: true,
|
||||
subscription: true,
|
||||
organisationClaim: true,
|
||||
teams: {
|
||||
where: {
|
||||
teamGroups: {
|
||||
some: {
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Organisation not found',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...organisation,
|
||||
teams: organisation.teams,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
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 SubscriptionSchema from '@documenso/prisma/generated/zod/modelSchema/SubscriptionSchema';
|
||||
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
|
||||
// export const getOrganisationMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'GET',
|
||||
// path: '/organisation/{teamReference}',
|
||||
// summary: 'Get organisation',
|
||||
// description: 'Get an organisation by ID or URL',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZGetOrganisationRequestSchema = z.object({
|
||||
organisationReference: z.string().describe('The ID or URL of the organisation.'),
|
||||
});
|
||||
|
||||
export const ZGetOrganisationResponseSchema = ZOrganisationSchema.extend({
|
||||
organisationGlobalSettings: OrganisationGlobalSettingsSchema,
|
||||
organisationClaim: OrganisationClaimSchema,
|
||||
subscription: SubscriptionSchema.nullable(),
|
||||
teams: z.array(
|
||||
TeamSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
createdAt: true,
|
||||
avatarImageId: true,
|
||||
organisationId: true,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type TGetOrganisationResponse = z.infer<typeof ZGetOrganisationResponseSchema>;
|
||||
@ -0,0 +1,58 @@
|
||||
import { getHighestOrganisationRoleInGroup } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetOrganisationsRequestSchema,
|
||||
ZGetOrganisationsResponseSchema,
|
||||
} from './get-organisations.types';
|
||||
|
||||
export const getOrganisationsRoute = authenticatedProcedure
|
||||
// .meta(getOrganisationsMeta)
|
||||
.input(ZGetOrganisationsRequestSchema)
|
||||
.output(ZGetOrganisationsResponseSchema)
|
||||
.query(async ({ ctx }) => {
|
||||
const { user } = ctx;
|
||||
|
||||
return getOrganisations({ userId: user.id });
|
||||
});
|
||||
|
||||
export const getOrganisations = async ({ userId }: { userId: number }) => {
|
||||
const organisations = await prisma.organisation.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
where: {
|
||||
organisationGroupMembers: {
|
||||
some: {
|
||||
organisationMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return organisations.map(({ groups, ...organisation }) => {
|
||||
const currentOrganisationRole = getHighestOrganisationRoleInGroup(groups);
|
||||
|
||||
return {
|
||||
...organisation,
|
||||
currentOrganisationRole: currentOrganisationRole,
|
||||
currentMemberId: organisation.members[0].id,
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZOrganisationManySchema } from '@documenso/lib/types/organisation';
|
||||
import OrganisationMemberRoleSchema from '@documenso/prisma/generated/zod/inputTypeSchemas/OrganisationMemberRoleSchema';
|
||||
|
||||
// export const getOrganisationsMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'GET',
|
||||
// path: '/organisation/teams',
|
||||
// summary: 'Get teams',
|
||||
// description: 'Get all teams you are a member of',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZGetOrganisationsRequestSchema = z.void();
|
||||
|
||||
export const ZGetOrganisationsResponseSchema = ZOrganisationManySchema.extend({
|
||||
currentOrganisationRole: OrganisationMemberRoleSchema,
|
||||
currentMemberId: z.string(),
|
||||
}).array();
|
||||
|
||||
export type TGetOrganisationsResponse = z.infer<typeof ZGetOrganisationsResponseSchema>;
|
||||
@ -0,0 +1,75 @@
|
||||
import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
|
||||
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 {
|
||||
ZLeaveOrganisationRequestSchema,
|
||||
ZLeaveOrganisationResponseSchema,
|
||||
} from './leave-organisation.types';
|
||||
|
||||
export const leaveOrganisationRoute = authenticatedProcedure
|
||||
.input(ZLeaveOrganisationRequestSchema)
|
||||
.output(ZLeaveOrganisationResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { organisationId } = input;
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({ organisationId, userId }),
|
||||
include: {
|
||||
organisationClaim: true,
|
||||
subscription: true,
|
||||
invites: {
|
||||
where: {
|
||||
status: OrganisationMemberInviteStatus.PENDING,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND);
|
||||
}
|
||||
|
||||
const { organisationClaim } = organisation;
|
||||
|
||||
const subscription = validateIfSubscriptionIsRequired(organisation.subscription);
|
||||
|
||||
const inviteCount = organisation.invites.length;
|
||||
const newMemberCount = organisation.members.length + inviteCount - 1;
|
||||
|
||||
if (subscription) {
|
||||
await syncMemberCountWithStripeSeatPlan(subscription, organisationClaim, newMemberCount);
|
||||
}
|
||||
|
||||
await prisma.organisationMember.delete({
|
||||
where: {
|
||||
userId_organisationId: {
|
||||
userId,
|
||||
organisationId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await jobs.triggerJob({
|
||||
name: 'send.organisation-member-left.email',
|
||||
payload: {
|
||||
organisationId: organisation.id,
|
||||
memberUserId: userId,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZLeaveOrganisationRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
});
|
||||
|
||||
export const ZLeaveOrganisationResponseSchema = z.void();
|
||||
@ -0,0 +1,99 @@
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { sendOrganisationMemberInviteEmail } from '@documenso/lib/server-only/organisation/create-organisation-member-invites';
|
||||
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZResendOrganisationMemberInviteRequestSchema,
|
||||
ZResendOrganisationMemberInviteResponseSchema,
|
||||
} from './resend-organisation-member-invite.types';
|
||||
|
||||
export const resendOrganisationMemberInviteRoute = authenticatedProcedure
|
||||
// .meta(resendOrganisationMemberInviteMeta)
|
||||
.input(ZResendOrganisationMemberInviteRequestSchema)
|
||||
.output(ZResendOrganisationMemberInviteResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { organisationId, invitationId } = input;
|
||||
|
||||
const userId = ctx.user.id;
|
||||
const userName = ctx.user.name || '';
|
||||
|
||||
await resendOrganisationMemberInvitation({
|
||||
userId,
|
||||
userName,
|
||||
organisationId,
|
||||
invitationId,
|
||||
});
|
||||
});
|
||||
|
||||
export type ResendOrganisationMemberInvitationOptions = {
|
||||
/**
|
||||
* The ID of the user who is initiating this action.
|
||||
*/
|
||||
userId: number;
|
||||
|
||||
/**
|
||||
* The name of the user who is initiating this action.
|
||||
*/
|
||||
userName: string;
|
||||
|
||||
/**
|
||||
* The ID of the organisation.
|
||||
*/
|
||||
organisationId: string;
|
||||
|
||||
/**
|
||||
* The IDs of the invitations to resend.
|
||||
*/
|
||||
invitationId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resend an email for a given member invite.
|
||||
*/
|
||||
export const resendOrganisationMemberInvitation = async ({
|
||||
userId,
|
||||
userName,
|
||||
organisationId,
|
||||
invitationId,
|
||||
}: ResendOrganisationMemberInvitationOptions): Promise<void> => {
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
include: {
|
||||
organisationGlobalSettings: true,
|
||||
invites: {
|
||||
where: {
|
||||
id: invitationId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError('OrganisationNotFound', {
|
||||
message: 'User is not a valid member of the team.',
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
const invitation = organisation.invites[0];
|
||||
|
||||
if (!invitation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Invitation does not exist',
|
||||
});
|
||||
}
|
||||
|
||||
await sendOrganisationMemberInviteEmail({
|
||||
email: invitation.email,
|
||||
token: invitation.token,
|
||||
senderName: userName,
|
||||
organisation,
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const resendOrganisationMemberInviteMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/organisation/member/resend-invite',
|
||||
// summary: 'Resend organisation member invite',
|
||||
// description: 'Resend a organisation member invite',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZResendOrganisationMemberInviteRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
invitationId: z.string(),
|
||||
});
|
||||
|
||||
export const ZResendOrganisationMemberInviteResponseSchema = z.void();
|
||||
60
packages/trpc/server/organisation-router/router.ts
Normal file
60
packages/trpc/server/organisation-router/router.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { router } from '../trpc';
|
||||
import { acceptOrganisationMemberInviteRoute } from './accept-organisation-member-invite';
|
||||
import { createOrganisationRoute } from './create-organisation';
|
||||
import { createOrganisationGroupRoute } from './create-organisation-group';
|
||||
import { createOrganisationMemberInvitesRoute } from './create-organisation-member-invites';
|
||||
import { declineOrganisationMemberInviteRoute } from './decline-organisation-member-invite';
|
||||
import { deleteOrganisationRoute } from './delete-organisation';
|
||||
import { deleteOrganisationGroupRoute } from './delete-organisation-group';
|
||||
import { deleteOrganisationMemberRoute } from './delete-organisation-member';
|
||||
import { deleteOrganisationMemberInvitesRoute } from './delete-organisation-member-invites';
|
||||
import { deleteOrganisationMembersRoute } from './delete-organisation-members';
|
||||
import { findOrganisationGroupsRoute } from './find-organisation-groups';
|
||||
import { findOrganisationMemberInvitesRoute } from './find-organisation-member-invites';
|
||||
import { findOrganisationMembersRoute } from './find-organisation-members';
|
||||
import { getOrganisationRoute } from './get-organisation';
|
||||
import { getOrganisationMemberInvitesRoute } from './get-organisation-member-invites';
|
||||
import { getOrganisationSessionRoute } from './get-organisation-session';
|
||||
import { getOrganisationsRoute } from './get-organisations';
|
||||
import { leaveOrganisationRoute } from './leave-organisation';
|
||||
import { resendOrganisationMemberInviteRoute } from './resend-organisation-member-invite';
|
||||
import { updateOrganisationRoute } from './update-organisation';
|
||||
import { updateOrganisationGroupRoute } from './update-organisation-group';
|
||||
import { updateOrganisationMemberRoute } from './update-organisation-members';
|
||||
import { updateOrganisationSettingsRoute } from './update-organisation-settings';
|
||||
|
||||
export const organisationRouter = router({
|
||||
get: getOrganisationRoute,
|
||||
getMany: getOrganisationsRoute,
|
||||
create: createOrganisationRoute,
|
||||
update: updateOrganisationRoute,
|
||||
delete: deleteOrganisationRoute,
|
||||
leave: leaveOrganisationRoute,
|
||||
member: {
|
||||
find: findOrganisationMembersRoute,
|
||||
update: updateOrganisationMemberRoute,
|
||||
delete: deleteOrganisationMemberRoute,
|
||||
deleteMany: deleteOrganisationMembersRoute,
|
||||
invite: {
|
||||
find: findOrganisationMemberInvitesRoute,
|
||||
getMany: getOrganisationMemberInvitesRoute,
|
||||
createMany: createOrganisationMemberInvitesRoute,
|
||||
deleteMany: deleteOrganisationMemberInvitesRoute,
|
||||
accept: acceptOrganisationMemberInviteRoute,
|
||||
decline: declineOrganisationMemberInviteRoute,
|
||||
resend: resendOrganisationMemberInviteRoute,
|
||||
},
|
||||
},
|
||||
group: {
|
||||
find: findOrganisationGroupsRoute,
|
||||
create: createOrganisationGroupRoute,
|
||||
update: updateOrganisationGroupRoute,
|
||||
delete: deleteOrganisationGroupRoute,
|
||||
},
|
||||
settings: {
|
||||
update: updateOrganisationSettingsRoute,
|
||||
},
|
||||
internal: {
|
||||
getOrganisationSession: getOrganisationSessionRoute,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,127 @@
|
||||
import { unique } from 'remeda';
|
||||
|
||||
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 { OrganisationGroupType } from '@documenso/prisma/generated/types';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateOrganisationGroupRequestSchema,
|
||||
ZUpdateOrganisationGroupResponseSchema,
|
||||
} from './update-organisation-group.types';
|
||||
|
||||
export const updateOrganisationGroupRoute = authenticatedProcedure
|
||||
// .meta(updateOrganisationGroupMeta)
|
||||
.input(ZUpdateOrganisationGroupRequestSchema)
|
||||
.output(ZUpdateOrganisationGroupResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { id, ...data } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
const organisationGroup = await prisma.organisationGroup.findFirst({
|
||||
where: {
|
||||
id,
|
||||
organisation: buildOrganisationWhereQuery({
|
||||
organisationId: undefined,
|
||||
userId: user.id,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
organisationGroupMembers: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisationGroup) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Organisation group not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (organisationGroup.type === OrganisationGroupType.INTERNAL_ORGANISATION) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You are not allowed to update internal organisation groups',
|
||||
});
|
||||
}
|
||||
|
||||
const currentUserOrganisationRole = await getMemberOrganisationRole({
|
||||
organisationId: organisationGroup.organisationId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
!isOrganisationRoleWithinUserHierarchy(
|
||||
currentUserOrganisationRole,
|
||||
organisationGroup.organisationRole,
|
||||
)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You are not allowed to update this organisation group',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
data.organisationRole &&
|
||||
!isOrganisationRoleWithinUserHierarchy(currentUserOrganisationRole, data.organisationRole)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You are not allowed to set an organisation role higher than your own',
|
||||
});
|
||||
}
|
||||
|
||||
const groupMemberIds = unique(data.memberIds || []);
|
||||
|
||||
const membersToDelete = organisationGroup.organisationGroupMembers.filter(
|
||||
(member) => !groupMemberIds.includes(member.organisationMemberId),
|
||||
);
|
||||
|
||||
const membersToCreate = groupMemberIds.filter(
|
||||
(id) =>
|
||||
!organisationGroup.organisationGroupMembers.some(
|
||||
(member) => member.organisationMemberId === id,
|
||||
),
|
||||
);
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.organisationGroup.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
organisationRole: data.organisationRole,
|
||||
name: data.name,
|
||||
},
|
||||
});
|
||||
|
||||
// Only run deletion if memberIds is defined.
|
||||
if (data.memberIds && membersToDelete.length > 0) {
|
||||
await tx.organisationGroupMember.deleteMany({
|
||||
where: {
|
||||
groupId: organisationGroup.id,
|
||||
organisationMemberId: { in: membersToDelete.map((m) => m.organisationMemberId) },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Only run creation if memberIds is defined.
|
||||
if (data.memberIds && membersToCreate.length > 0) {
|
||||
await tx.organisationGroupMember.createMany({
|
||||
data: membersToCreate.map((id) => ({
|
||||
id: generateDatabaseId('group_member'),
|
||||
groupId: organisationGroup.id,
|
||||
organisationMemberId: id,
|
||||
})),
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
import { OrganisationMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const updateOrganisationGroupMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/organisation/groups/{id}',
|
||||
// summary: 'Update organisation group',
|
||||
// description: 'Update an existing group for a organisation',
|
||||
// tags: ['Organisation'],
|
||||
// requiredScopes: ['personal:organisation:write'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZUpdateOrganisationGroupRequestSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullable().optional(),
|
||||
organisationRole: z.nativeEnum(OrganisationMemberRole).optional(),
|
||||
memberIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const ZUpdateOrganisationGroupResponseSchema = z.void();
|
||||
|
||||
export type TUpdateOrganisationGroupRequest = z.infer<typeof ZUpdateOrganisationGroupRequestSchema>;
|
||||
@ -0,0 +1,156 @@
|
||||
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 { generateDatabaseId } from '@documenso/lib/universal/id';
|
||||
import {
|
||||
buildOrganisationWhereQuery,
|
||||
getHighestOrganisationRoleInGroup,
|
||||
isOrganisationRoleWithinUserHierarchy,
|
||||
} from '@documenso/lib/utils/organisations';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZUpdateOrganisationMemberRequestSchema,
|
||||
ZUpdateOrganisationMemberResponseSchema,
|
||||
} from './update-organisation-members.types';
|
||||
|
||||
export const updateOrganisationMemberRoute = authenticatedProcedure
|
||||
// .meta(updateOrganisationMemberMeta)
|
||||
.input(ZUpdateOrganisationMemberRequestSchema)
|
||||
.output(ZUpdateOrganisationMemberResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { organisationId, organisationMemberId, data } = input;
|
||||
const userId = ctx.user.id;
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
include: {
|
||||
groups: {
|
||||
where: {
|
||||
type: OrganisationGroupType.INTERNAL_ORGANISATION,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
include: {
|
||||
organisationGroupMembers: {
|
||||
include: {
|
||||
group: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Organisation not found' });
|
||||
}
|
||||
|
||||
const currentUser = organisation.members.find((member) => member.userId === userId);
|
||||
|
||||
const organisationMemberToUpdate = organisation.members.find(
|
||||
(member) => member.id === organisationMemberId,
|
||||
);
|
||||
|
||||
if (!organisationMemberToUpdate || !currentUser) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Organisation member does not exist' });
|
||||
}
|
||||
|
||||
if (organisationMemberToUpdate.userId === organisation.ownerUserId) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'Cannot update the owner' });
|
||||
}
|
||||
|
||||
const currentUserOrganisationRoles = currentUser.organisationGroupMembers.filter(
|
||||
({ group }) => group.type === OrganisationGroupType.INTERNAL_ORGANISATION,
|
||||
);
|
||||
|
||||
if (currentUserOrganisationRoles.length !== 1) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Current user has multiple internal organisation roles',
|
||||
});
|
||||
}
|
||||
|
||||
const currentUserOrganisationRole = currentUserOrganisationRoles[0].group.organisationRole;
|
||||
const currentMemberToUpdateOrganisationRole = getHighestOrganisationRoleInGroup(
|
||||
organisationMemberToUpdate.organisationGroupMembers.flatMap((member) => member.group),
|
||||
);
|
||||
|
||||
const isMemberToUpdateHigherRole = !isOrganisationRoleWithinUserHierarchy(
|
||||
currentUserOrganisationRole,
|
||||
currentMemberToUpdateOrganisationRole,
|
||||
);
|
||||
|
||||
if (isMemberToUpdateHigherRole) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Cannot update a member with a higher role',
|
||||
});
|
||||
}
|
||||
|
||||
const isNewMemberRoleHigherThanCurrentRole = !isOrganisationRoleWithinUserHierarchy(
|
||||
currentUserOrganisationRole,
|
||||
data.role,
|
||||
);
|
||||
|
||||
if (isNewMemberRoleHigherThanCurrentRole) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Cannot give a member a role higher than the user initating the update',
|
||||
});
|
||||
}
|
||||
|
||||
const currentMemberGroup = organisation.groups.find(
|
||||
(group) => group.organisationRole === currentMemberToUpdateOrganisationRole,
|
||||
);
|
||||
|
||||
const newMemberGroup = organisation.groups.find(
|
||||
(group) => group.organisationRole === data.role,
|
||||
);
|
||||
|
||||
if (!currentMemberGroup) {
|
||||
console.error('[CRITICAL]: Missing internal group');
|
||||
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Current member group not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (!newMemberGroup) {
|
||||
console.error('[CRITICAL]: Missing internal group');
|
||||
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'New member group not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Switch member to new internal group role.
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.organisationGroupMember.delete({
|
||||
where: {
|
||||
organisationMemberId_groupId: {
|
||||
organisationMemberId: organisationMemberToUpdate.id,
|
||||
groupId: currentMemberGroup.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tx.organisationGroupMember.create({
|
||||
data: {
|
||||
id: generateDatabaseId('group_member'),
|
||||
organisationMemberId: organisationMemberToUpdate.id,
|
||||
groupId: newMemberGroup.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
import { OrganisationMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const updateOrganisationMemberMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/organisation/member/update',
|
||||
// summary: 'Update organisation member',
|
||||
// description: 'Update organisation member',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZUpdateOrganisationMemberRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
organisationMemberId: z.string(),
|
||||
data: z.object({
|
||||
role: z.nativeEnum(OrganisationMemberRole),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZUpdateOrganisationMemberResponseSchema = z.void();
|
||||
@ -0,0 +1,100 @@
|
||||
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 {
|
||||
ZUpdateOrganisationSettingsRequestSchema,
|
||||
ZUpdateOrganisationSettingsResponseSchema,
|
||||
} from './update-organisation-settings.types';
|
||||
|
||||
export const updateOrganisationSettingsRoute = authenticatedProcedure
|
||||
.input(ZUpdateOrganisationSettingsRequestSchema)
|
||||
.output(ZUpdateOrganisationSettingsResponseSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { user } = ctx;
|
||||
const { organisationId, data } = input;
|
||||
|
||||
const {
|
||||
// Document related settings.
|
||||
documentVisibility,
|
||||
documentLanguage,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
// Branding related settings.
|
||||
brandingEnabled,
|
||||
brandingLogo,
|
||||
brandingUrl,
|
||||
brandingCompanyDetails,
|
||||
} = data;
|
||||
|
||||
if (Object.values(data).length === 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'No settings to update',
|
||||
});
|
||||
}
|
||||
|
||||
const organisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId: user.id,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
include: {
|
||||
organisationGlobalSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organisation) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to update this organisation.',
|
||||
});
|
||||
}
|
||||
|
||||
const derivedTypedSignatureEnabled =
|
||||
typedSignatureEnabled ?? organisation.organisationGlobalSettings.typedSignatureEnabled;
|
||||
const derivedUploadSignatureEnabled =
|
||||
uploadSignatureEnabled ?? organisation.organisationGlobalSettings.uploadSignatureEnabled;
|
||||
const derivedDrawSignatureEnabled =
|
||||
drawSignatureEnabled ?? organisation.organisationGlobalSettings.drawSignatureEnabled;
|
||||
|
||||
if (
|
||||
derivedTypedSignatureEnabled === false &&
|
||||
derivedUploadSignatureEnabled === false &&
|
||||
derivedDrawSignatureEnabled === false
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'At least one signature type must be enabled',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.organisation.update({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
data: {
|
||||
organisationGlobalSettings: {
|
||||
update: {
|
||||
// Document related settings.
|
||||
documentVisibility,
|
||||
documentLanguage,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
typedSignatureEnabled,
|
||||
uploadSignatureEnabled,
|
||||
drawSignatureEnabled,
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled,
|
||||
brandingLogo,
|
||||
brandingUrl,
|
||||
brandingCompanyDetails,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,26 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
|
||||
export const ZUpdateOrganisationSettingsRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
data: z.object({
|
||||
// Document related settings.
|
||||
documentVisibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
|
||||
includeSenderDetails: z.boolean().optional(),
|
||||
includeSigningCertificate: z.boolean().optional(),
|
||||
typedSignatureEnabled: z.boolean().optional(),
|
||||
uploadSignatureEnabled: z.boolean().optional(),
|
||||
drawSignatureEnabled: z.boolean().optional(),
|
||||
|
||||
// Branding related settings.
|
||||
brandingEnabled: z.boolean().optional(),
|
||||
brandingLogo: z.string().optional(),
|
||||
brandingUrl: z.string().optional(),
|
||||
brandingCompanyDetails: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZUpdateOrganisationSettingsResponseSchema = 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 {
|
||||
ZUpdateOrganisationRequestSchema,
|
||||
ZUpdateOrganisationResponseSchema,
|
||||
} from './update-organisation.types';
|
||||
|
||||
export const updateOrganisationRoute = authenticatedProcedure
|
||||
// .meta(updateOrganisationMeta)
|
||||
.input(ZUpdateOrganisationRequestSchema)
|
||||
.output(ZUpdateOrganisationResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { organisationId, data } = input;
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
// Check if organisation exists and user has access to it
|
||||
const existingOrganisation = await prisma.organisation.findFirst({
|
||||
where: buildOrganisationWhereQuery({
|
||||
organisationId,
|
||||
userId,
|
||||
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!existingOrganisation) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Organisation not found',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.organisation.update({
|
||||
where: {
|
||||
id: organisationId,
|
||||
},
|
||||
data: {
|
||||
name: data.name,
|
||||
url: data.url,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZTeamUrlSchema } from '../team-router/schema';
|
||||
import { ZCreateOrganisationRequestSchema } from './create-organisation.types';
|
||||
|
||||
// export const updateOrganisationMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/organisation/{teamId}',
|
||||
// summary: 'Update organisation',
|
||||
// description: 'Update an organisation',
|
||||
// tags: ['Organisation'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZUpdateOrganisationRequestSchema = z.object({
|
||||
data: ZCreateOrganisationRequestSchema.pick({
|
||||
name: true,
|
||||
}).extend({
|
||||
url: ZTeamUrlSchema,
|
||||
}),
|
||||
organisationId: z.string(),
|
||||
});
|
||||
|
||||
export const ZUpdateOrganisationResponseSchema = z.void();
|
||||
@ -1,25 +1,16 @@
|
||||
import { SubscriptionStatus } from '@prisma/client';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import type { SetAvatarImageOptions } from '@documenso/lib/server-only/profile/set-avatar-image';
|
||||
import { setAvatarImage } from '@documenso/lib/server-only/profile/set-avatar-image';
|
||||
import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id';
|
||||
import { createBillingPortal } from '@documenso/lib/server-only/user/create-billing-portal';
|
||||
import { createCheckoutSession } from '@documenso/lib/server-only/user/create-checkout-session';
|
||||
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
|
||||
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
|
||||
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
||||
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
|
||||
import { updatePublicProfile } from '@documenso/lib/server-only/user/update-public-profile';
|
||||
|
||||
import { adminProcedure, authenticatedProcedure, router } from '../trpc';
|
||||
import {
|
||||
ZCreateCheckoutSessionRequestSchema,
|
||||
ZFindUserSecurityAuditLogsSchema,
|
||||
ZRetrieveUserByIdQuerySchema,
|
||||
ZSetProfileImageMutationSchema,
|
||||
ZUpdateProfileMutationSchema,
|
||||
ZUpdatePublicProfileMutationSchema,
|
||||
} from './schema';
|
||||
|
||||
export const profileRouter = router({
|
||||
@ -38,31 +29,6 @@ export const profileRouter = router({
|
||||
return await getUserById({ id });
|
||||
}),
|
||||
|
||||
createBillingPortal: authenticatedProcedure.mutation(async ({ ctx }) => {
|
||||
return await createBillingPortal({
|
||||
user: {
|
||||
id: ctx.user.id,
|
||||
customerId: ctx.user.customerId,
|
||||
email: ctx.user.email,
|
||||
name: ctx.user.name,
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
createCheckoutSession: authenticatedProcedure
|
||||
.input(ZCreateCheckoutSessionRequestSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return await createCheckoutSession({
|
||||
user: {
|
||||
id: ctx.user.id,
|
||||
customerId: ctx.user.customerId,
|
||||
email: ctx.user.email,
|
||||
name: ctx.user.name,
|
||||
},
|
||||
priceId: input.priceId,
|
||||
});
|
||||
}),
|
||||
|
||||
updateProfile: authenticatedProcedure
|
||||
.input(ZUpdateProfileMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@ -76,37 +42,6 @@ export const profileRouter = router({
|
||||
});
|
||||
}),
|
||||
|
||||
updatePublicProfile: authenticatedProcedure
|
||||
.input(ZUpdatePublicProfileMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { url, bio, enabled } = input;
|
||||
|
||||
if (IS_BILLING_ENABLED() && url !== undefined && url.length < 6) {
|
||||
const subscriptions = await getSubscriptionsByUserId({
|
||||
userId: ctx.user.id,
|
||||
}).then((subscriptions) =>
|
||||
subscriptions.filter((s) => s.status === SubscriptionStatus.ACTIVE),
|
||||
);
|
||||
|
||||
if (subscriptions.length === 0) {
|
||||
throw new AppError('PREMIUM_PROFILE_URL', {
|
||||
message: 'Only subscribers can have a username shorter than 6 characters',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const user = await updatePublicProfile({
|
||||
userId: ctx.user.id,
|
||||
data: {
|
||||
url,
|
||||
bio,
|
||||
enabled,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, url: user.url };
|
||||
}),
|
||||
|
||||
deleteAccount: authenticatedProcedure.mutation(async ({ ctx }) => {
|
||||
return await deleteUser({
|
||||
id: ctx.user.id,
|
||||
@ -116,11 +51,29 @@ export const profileRouter = router({
|
||||
setProfileImage: authenticatedProcedure
|
||||
.input(ZSetProfileImageMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { bytes, teamId } = input;
|
||||
const { bytes, teamId, organisationId } = input;
|
||||
|
||||
let target: SetAvatarImageOptions['target'] = {
|
||||
type: 'user',
|
||||
};
|
||||
|
||||
if (teamId) {
|
||||
target = {
|
||||
type: 'team',
|
||||
teamId,
|
||||
};
|
||||
}
|
||||
|
||||
if (organisationId) {
|
||||
target = {
|
||||
type: 'organisation',
|
||||
organisationId,
|
||||
};
|
||||
}
|
||||
|
||||
return await setAvatarImage({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
target,
|
||||
bytes,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const MAX_PROFILE_BIO_LENGTH = 256;
|
||||
|
||||
export const ZFindUserSecurityAuditLogsSchema = z.object({
|
||||
page: z.number().optional(),
|
||||
perPage: z.number().optional(),
|
||||
@ -15,10 +13,6 @@ export const ZRetrieveUserByIdQuerySchema = z.object({
|
||||
|
||||
export type TRetrieveUserByIdQuerySchema = z.infer<typeof ZRetrieveUserByIdQuerySchema>;
|
||||
|
||||
export const ZCreateCheckoutSessionRequestSchema = z.object({
|
||||
priceId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const ZUpdateProfileMutationSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
signature: z.string(),
|
||||
@ -26,30 +20,10 @@ export const ZUpdateProfileMutationSchema = z.object({
|
||||
|
||||
export type TUpdateProfileMutationSchema = z.infer<typeof ZUpdateProfileMutationSchema>;
|
||||
|
||||
export const ZUpdatePublicProfileMutationSchema = z.object({
|
||||
bio: z
|
||||
.string()
|
||||
.max(MAX_PROFILE_BIO_LENGTH, {
|
||||
message: `Bio must be shorter than ${MAX_PROFILE_BIO_LENGTH + 1} characters`,
|
||||
})
|
||||
.optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
url: z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.min(1, { message: 'Please enter a valid username.' })
|
||||
.regex(/^[a-z0-9-]+$/, {
|
||||
message: 'Username can only container alphanumeric characters and dashes.',
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type TUpdatePublicProfileMutationSchema = z.infer<typeof ZUpdatePublicProfileMutationSchema>;
|
||||
|
||||
export const ZSetProfileImageMutationSchema = z.object({
|
||||
bytes: z.string().nullish(),
|
||||
teamId: z.number().min(1).nullish(),
|
||||
teamId: z.number().min(1).nullable(),
|
||||
organisationId: z.string().nullable(),
|
||||
});
|
||||
|
||||
export type TSetProfileImageMutationSchema = z.infer<typeof ZSetProfileImageMutationSchema>;
|
||||
|
||||
@ -26,8 +26,8 @@ export const ZCreateRecipientSchema = z.object({
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
accessAuth: ZRecipientAccessAuthTypesSchema.optional().nullable(),
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
||||
accessAuth: z.array(ZRecipientAccessAuthTypesSchema).optional().default([]),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
});
|
||||
|
||||
export const ZUpdateRecipientSchema = z.object({
|
||||
@ -36,8 +36,8 @@ export const ZUpdateRecipientSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
role: z.nativeEnum(RecipientRole).optional(),
|
||||
signingOrder: z.number().optional(),
|
||||
accessAuth: ZRecipientAccessAuthTypesSchema.optional().nullable(),
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
||||
accessAuth: z.array(ZRecipientAccessAuthTypesSchema).optional().default([]),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
});
|
||||
|
||||
export const ZCreateDocumentRecipientRequestSchema = z.object({
|
||||
@ -106,7 +106,7 @@ export const ZSetDocumentRecipientsRequestSchema = z
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
}),
|
||||
),
|
||||
})
|
||||
@ -190,7 +190,7 @@ export const ZSetTemplateRecipientsRequestSchema = z
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
||||
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { adminRouter } from './admin-router/router';
|
||||
import { apiTokenRouter } from './api-token-router/router';
|
||||
import { authRouter } from './auth-router/router';
|
||||
import { billingRouter } from './billing/router';
|
||||
import { documentRouter } from './document-router/router';
|
||||
import { embeddingPresignRouter } from './embedding-router/_router';
|
||||
import { fieldRouter } from './field-router/router';
|
||||
import { folderRouter } from './folder-router/router';
|
||||
import { organisationRouter } from './organisation-router/router';
|
||||
import { profileRouter } from './profile-router/router';
|
||||
import { recipientRouter } from './recipient-router/router';
|
||||
import { shareLinkRouter } from './share-link-router/router';
|
||||
@ -15,12 +17,14 @@ import { webhookRouter } from './webhook-router/router';
|
||||
|
||||
export const appRouter = router({
|
||||
auth: authRouter,
|
||||
billing: billingRouter,
|
||||
profile: profileRouter,
|
||||
document: documentRouter,
|
||||
field: fieldRouter,
|
||||
folder: folderRouter,
|
||||
recipient: recipientRouter,
|
||||
admin: adminRouter,
|
||||
organisation: organisationRouter,
|
||||
shareLink: shareLinkRouter,
|
||||
apiToken: apiTokenRouter,
|
||||
team: teamRouter,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { procedure } from '../trpc';
|
||||
@ -26,26 +27,12 @@ export const getDocumentInternalUrlForQRCodeRoute = procedure
|
||||
},
|
||||
{
|
||||
id: documentId,
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
team: buildTeamWhereQuery({ teamId: undefined, userId: ctx.user.id }),
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
team: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -53,9 +40,5 @@ export const getDocumentInternalUrlForQRCodeRoute = procedure
|
||||
return null;
|
||||
}
|
||||
|
||||
if (document.team) {
|
||||
return `${NEXT_PUBLIC_WEBAPP_URL()}/t/${document.team.url}/documents/${document.id}`;
|
||||
}
|
||||
|
||||
return `${NEXT_PUBLIC_WEBAPP_URL()}/documents/${document.id}`;
|
||||
return `${NEXT_PUBLIC_WEBAPP_URL()}/t/${document.team.url}/documents/${document.id}`;
|
||||
});
|
||||
|
||||
107
packages/trpc/server/team-router/create-team-groups.ts
Normal file
107
packages/trpc/server/team-router/create-team-groups.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import {
|
||||
ALLOWED_TEAM_GROUP_TYPES,
|
||||
TEAM_MEMBER_ROLE_PERMISSIONS_MAP,
|
||||
} from '@documenso/lib/constants/teams';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getMemberRoles } from '@documenso/lib/server-only/team/get-member-roles';
|
||||
import { generateDatabaseId } from '@documenso/lib/universal/id';
|
||||
import { buildTeamWhereQuery, isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import {
|
||||
OrganisationGroupType,
|
||||
OrganisationMemberRole,
|
||||
TeamMemberRole,
|
||||
} from '@documenso/prisma/generated/types';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZCreateTeamGroupsRequestSchema,
|
||||
ZCreateTeamGroupsResponseSchema,
|
||||
} from './create-team-groups.types';
|
||||
|
||||
export const createTeamGroupsRoute = authenticatedProcedure
|
||||
// .meta(createTeamGroupsMeta)
|
||||
.input(ZCreateTeamGroupsRequestSchema)
|
||||
.output(ZCreateTeamGroupsResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId, groups } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId: user.id,
|
||||
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
}),
|
||||
include: {
|
||||
organisation: {
|
||||
include: {
|
||||
groups: {
|
||||
include: {
|
||||
teamGroups: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const { teamRole: currentUserTeamRole } = await getMemberRoles({
|
||||
teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
const isValid = groups.every((group) => {
|
||||
const organisationGroup = team.organisation.groups.find(
|
||||
({ id }) => id === group.organisationGroupId,
|
||||
);
|
||||
|
||||
// Only allow specific organisation groups to be used as a reference for team groups.
|
||||
if (!organisationGroup?.type || !ALLOWED_TEAM_GROUP_TYPES.includes(organisationGroup.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The "EVERYONE" organisation group can only have the "TEAM MEMBER" role for now.
|
||||
if (
|
||||
organisationGroup.type === OrganisationGroupType.INTERNAL_ORGANISATION &&
|
||||
organisationGroup.organisationRole === OrganisationMemberRole.MEMBER &&
|
||||
group.teamRole !== TeamMemberRole.MEMBER
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that the group is not already added to the team.
|
||||
if (organisationGroup.teamGroups.some((teamGroup) => teamGroup.teamId === teamId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that the user has permission to add the group to the team.
|
||||
if (!isTeamRoleWithinUserHierarchy(currentUserTeamRole, group.teamRole)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid groups',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.teamGroup.createMany({
|
||||
data: groups.map((group) => ({
|
||||
id: generateDatabaseId('team_group'),
|
||||
teamId,
|
||||
organisationGroupId: group.organisationGroupId,
|
||||
teamRole: group.teamRole,
|
||||
})),
|
||||
});
|
||||
});
|
||||
28
packages/trpc/server/team-router/create-team-groups.types.ts
Normal file
28
packages/trpc/server/team-router/create-team-groups.types.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const createTeamGroupsMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/team/{teamId}/groups',
|
||||
// summary: 'Create team group',
|
||||
// description: 'Create a new group for a team',
|
||||
// tags: ['Team'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZCreateTeamGroupsRequestSchema = z.object({
|
||||
teamId: z.number(),
|
||||
groups: z.array(
|
||||
z.object({
|
||||
teamRole: z.nativeEnum(TeamMemberRole).describe('The team role to assign to the group'),
|
||||
organisationGroupId: z
|
||||
.string()
|
||||
.describe(
|
||||
'The ID of the organisation group to create the team group from. Members from the organisation group will be assigned automatically to this team group.',
|
||||
),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export const ZCreateTeamGroupsResponseSchema = z.void();
|
||||
153
packages/trpc/server/team-router/create-team-members.ts
Normal file
153
packages/trpc/server/team-router/create-team-members.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import { OrganisationGroupType, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getMemberRoles } from '@documenso/lib/server-only/team/get-member-roles';
|
||||
import { generateDatabaseId } from '@documenso/lib/universal/id';
|
||||
import { buildTeamWhereQuery, isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZCreateTeamMembersRequestSchema,
|
||||
ZCreateTeamMembersResponseSchema,
|
||||
} from './create-team-members.types';
|
||||
|
||||
export const createTeamMembersRoute = authenticatedProcedure
|
||||
.input(ZCreateTeamMembersRequestSchema)
|
||||
.output(ZCreateTeamMembersResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamId, organisationMembers } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
return await createTeamMembers({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
membersToCreate: organisationMembers,
|
||||
});
|
||||
});
|
||||
|
||||
type CreateTeamMembersOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
membersToCreate: {
|
||||
organisationMemberId: string;
|
||||
teamRole: TeamMemberRole;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const createTeamMembers = async ({
|
||||
userId,
|
||||
teamId,
|
||||
membersToCreate,
|
||||
}: CreateTeamMembersOptions) => {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId,
|
||||
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
}),
|
||||
include: {
|
||||
organisation: {
|
||||
include: {
|
||||
members: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
teamGroups: {
|
||||
where: {
|
||||
organisationGroup: {
|
||||
type: OrganisationGroupType.INTERNAL_TEAM,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organisationGroup: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team not found or missing permissions',
|
||||
});
|
||||
}
|
||||
|
||||
const isMembersPartOfOrganisation = membersToCreate.every((member) =>
|
||||
team.organisation.members.some(({ id }) => id === member.organisationMemberId),
|
||||
);
|
||||
|
||||
if (!isMembersPartOfOrganisation) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Some member IDs do not exist',
|
||||
});
|
||||
}
|
||||
|
||||
const teamMemberGroup = team.teamGroups.find(
|
||||
(group) =>
|
||||
group.organisationGroup.type === OrganisationGroupType.INTERNAL_TEAM &&
|
||||
group.teamId === teamId &&
|
||||
group.teamRole === TeamMemberRole.MEMBER,
|
||||
);
|
||||
|
||||
const teamManagerGroup = team.teamGroups.find(
|
||||
(group) =>
|
||||
group.organisationGroup.type === OrganisationGroupType.INTERNAL_TEAM &&
|
||||
group.teamId === teamId &&
|
||||
group.teamRole === TeamMemberRole.MANAGER,
|
||||
);
|
||||
|
||||
const teamAdminGroup = team.teamGroups.find(
|
||||
(group) =>
|
||||
group.organisationGroup.type === OrganisationGroupType.INTERNAL_TEAM &&
|
||||
group.teamId === teamId &&
|
||||
group.teamRole === TeamMemberRole.ADMIN,
|
||||
);
|
||||
|
||||
if (!teamMemberGroup || !teamManagerGroup || !teamAdminGroup) {
|
||||
console.error({
|
||||
message: 'Team groups not found.',
|
||||
teamMemberGroup: Boolean(teamMemberGroup),
|
||||
teamManagerGroup: Boolean(teamManagerGroup),
|
||||
teamAdminGroup: Boolean(teamAdminGroup),
|
||||
});
|
||||
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team groups not found.',
|
||||
});
|
||||
}
|
||||
|
||||
const { teamRole: currentUserTeamRole } = await getMemberRoles({
|
||||
teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
!membersToCreate.every((member) =>
|
||||
isTeamRoleWithinUserHierarchy(currentUserTeamRole, member.teamRole),
|
||||
)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'Cannot add a member with a role higher than your own',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.organisationGroupMember.createMany({
|
||||
data: membersToCreate.map((member) => ({
|
||||
id: generateDatabaseId('group_member'),
|
||||
organisationMemberId: member.organisationMemberId,
|
||||
groupId: match(member.teamRole)
|
||||
.with(TeamMemberRole.MEMBER, () => teamMemberGroup.organisationGroupId)
|
||||
.with(TeamMemberRole.MANAGER, () => teamManagerGroup.organisationGroupId)
|
||||
.with(TeamMemberRole.ADMIN, () => teamAdminGroup.organisationGroupId)
|
||||
.exhaustive(),
|
||||
})),
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { TeamMemberRole } from '@documenso/prisma/generated/types';
|
||||
|
||||
export const ZCreateTeamMembersRequestSchema = z.object({
|
||||
teamId: z.number(),
|
||||
organisationMembers: z
|
||||
.array(
|
||||
z.object({
|
||||
organisationMemberId: z.string().min(1),
|
||||
teamRole: z.nativeEnum(TeamMemberRole).describe('The team role to add the user as'),
|
||||
}),
|
||||
)
|
||||
.min(1)
|
||||
.superRefine((items, ctx) => {
|
||||
const seen = new Map<string, number>();
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const id = item.organisationMemberId;
|
||||
if (seen.has(id)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'IDs must be unique',
|
||||
path: [index, 'organisationMemberId'], // relative to organisationMembers
|
||||
});
|
||||
} else {
|
||||
seen.set(id, index);
|
||||
}
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZCreateTeamMembersResponseSchema = z.void();
|
||||
|
||||
export type TCreateTeamMembersRequestSchema = z.infer<typeof ZCreateTeamMembersRequestSchema>;
|
||||
21
packages/trpc/server/team-router/create-team.ts
Normal file
21
packages/trpc/server/team-router/create-team.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { createTeam } from '@documenso/lib/server-only/team/create-team';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZCreateTeamRequestSchema, ZCreateTeamResponseSchema } from './create-team.types';
|
||||
|
||||
export const createTeamRoute = authenticatedProcedure
|
||||
// .meta(createOrganisationGroupMeta)
|
||||
.input(ZCreateTeamRequestSchema)
|
||||
.output(ZCreateTeamResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamName, teamUrl, organisationId, inheritMembers } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
return await createTeam({
|
||||
userId: user.id,
|
||||
teamName,
|
||||
teamUrl,
|
||||
organisationId,
|
||||
inheritMembers,
|
||||
});
|
||||
});
|
||||
29
packages/trpc/server/team-router/create-team.types.ts
Normal file
29
packages/trpc/server/team-router/create-team.types.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZTeamUrlSchema } from './schema';
|
||||
import { ZTeamNameSchema } from './schema';
|
||||
|
||||
// export const createTeamMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/team/create',
|
||||
// summary: 'Create team',
|
||||
// description: 'Create a new team',
|
||||
// tags: ['Team'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZCreateTeamRequestSchema = z.object({
|
||||
organisationId: z.string(),
|
||||
teamName: ZTeamNameSchema,
|
||||
teamUrl: ZTeamUrlSchema,
|
||||
inheritMembers: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'Whether to automatically assign all current and future organisation members to the new team. Defaults to true.',
|
||||
),
|
||||
});
|
||||
|
||||
export const ZCreateTeamResponseSchema = z.void();
|
||||
|
||||
export type TCreateTeamRequest = z.infer<typeof ZCreateTeamRequestSchema>;
|
||||
84
packages/trpc/server/team-router/delete-team-group.ts
Normal file
84
packages/trpc/server/team-router/delete-team-group.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getMemberRoles } from '@documenso/lib/server-only/team/get-member-roles';
|
||||
import { buildTeamWhereQuery, isTeamRoleWithinUserHierarchy } from '@documenso/lib/utils/teams';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { OrganisationGroupType, OrganisationMemberRole } from '@documenso/prisma/generated/types';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZDeleteTeamGroupRequestSchema,
|
||||
ZDeleteTeamGroupResponseSchema,
|
||||
} from './delete-team-group.types';
|
||||
|
||||
export const deleteTeamGroupRoute = authenticatedProcedure
|
||||
// .meta(deleteTeamGroupMeta)
|
||||
.input(ZDeleteTeamGroupRequestSchema)
|
||||
.output(ZDeleteTeamGroupResponseSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { teamGroupId, teamId } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: buildTeamWhereQuery({
|
||||
teamId,
|
||||
userId: user.id,
|
||||
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const group = await prisma.teamGroup.findFirst({
|
||||
where: {
|
||||
id: teamGroupId,
|
||||
team: {
|
||||
id: teamId,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organisationGroup: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Team group not found',
|
||||
});
|
||||
}
|
||||
|
||||
// You cannot delete internal organisation groups.
|
||||
// The only exception is deleting the "member" organisation group which is used to allow
|
||||
// all organisation members to access a team.
|
||||
if (
|
||||
group.organisationGroup.type === OrganisationGroupType.INTERNAL_ORGANISATION &&
|
||||
group.organisationGroup.organisationRole !== OrganisationMemberRole.MEMBER
|
||||
) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You are not allowed to delete internal organisaion groups',
|
||||
});
|
||||
}
|
||||
|
||||
const { teamRole: currentUserTeamRole } = await getMemberRoles({
|
||||
teamId,
|
||||
reference: {
|
||||
type: 'User',
|
||||
id: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isTeamRoleWithinUserHierarchy(currentUserTeamRole, group.teamRole)) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You are not allowed to delete this team group',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.teamGroup.delete({
|
||||
where: {
|
||||
id: teamGroupId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
});
|
||||
18
packages/trpc/server/team-router/delete-team-group.types.ts
Normal file
18
packages/trpc/server/team-router/delete-team-group.types.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// export const deleteTeamGroupMeta: TrpcOpenApiMeta = {
|
||||
// openapi: {
|
||||
// method: 'POST',
|
||||
// path: '/team/groups/{id}/delete',
|
||||
// summary: 'Delete team group',
|
||||
// description: 'Delete an existing group for a team',
|
||||
// tags: ['Team'],
|
||||
// },
|
||||
// };
|
||||
|
||||
export const ZDeleteTeamGroupRequestSchema = z.object({
|
||||
teamId: z.number(),
|
||||
teamGroupId: z.string(),
|
||||
});
|
||||
|
||||
export const ZDeleteTeamGroupResponseSchema = z.void();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user