feat: add organisations (#1820)

This commit is contained in:
David Nguyen
2025-06-10 11:49:52 +10:00
committed by GitHub
parent 0b37f19641
commit e6dc237ad2
631 changed files with 37616 additions and 25695 deletions

View File

@ -1,15 +1,8 @@
import type { Template, TemplateDirectLink } from '@prisma/client';
import {
SubscriptionStatus,
type TeamProfile,
TemplateType,
type UserProfile,
} from '@prisma/client';
import { type TeamProfile, TemplateType } from '@prisma/client';
import { getCommunityPlanPriceIds } from '@documenso/ee/server-only/stripe/get-community-plan-prices';
import { prisma } from '@documenso/prisma';
import { IS_BILLING_ENABLED } from '../../constants/app';
import { AppError, AppErrorCode } from '../../errors/app-error';
export type GetPublicProfileByUrlOptions = {
@ -23,7 +16,7 @@ type PublicDirectLinkTemplate = Template & {
};
};
type BaseResponse = {
type GetPublicProfileByUrlResponse = {
url: string;
name: string;
avatarImageId?: string | null;
@ -32,155 +25,56 @@ type BaseResponse = {
since: Date;
};
templates: PublicDirectLinkTemplate[];
profile: TeamProfile;
};
type GetPublicProfileByUrlResponse = BaseResponse &
(
| {
type: 'User';
profile: UserProfile;
}
| {
type: 'Team';
profile: TeamProfile;
}
);
/**
* Get the user or team public profile by URL.
*/
export const getPublicProfileByUrl = async ({
profileUrl,
}: GetPublicProfileByUrlOptions): Promise<GetPublicProfileByUrlResponse> => {
const [user, team] = await Promise.all([
prisma.user.findFirst({
where: {
url: profileUrl,
profile: {
enabled: true,
const team = await prisma.team.findFirst({
where: {
url: profileUrl,
profile: {
enabled: true,
},
},
include: {
profile: true,
templates: {
where: {
directLink: {
enabled: true,
},
type: TemplateType.PUBLIC,
},
include: {
directLink: true,
},
},
include: {
profile: true,
templates: {
where: {
directLink: {
enabled: true,
},
type: TemplateType.PUBLIC,
},
include: {
directLink: true,
},
},
// Subscriptions and teamMembers are used to calculate the badges.
subscriptions: {
where: {
status: SubscriptionStatus.ACTIVE,
},
},
teamMembers: {
select: {
createdAt: true,
},
orderBy: {
createdAt: 'asc',
},
},
},
}),
prisma.team.findFirst({
where: {
url: profileUrl,
profile: {
enabled: true,
},
},
include: {
profile: true,
templates: {
where: {
directLink: {
enabled: true,
},
type: TemplateType.PUBLIC,
},
include: {
directLink: true,
},
},
},
}),
]);
},
});
// Log as critical error.
if (user?.profile && team?.profile) {
console.error('Profile URL is ambiguous', { profileUrl, userId: user.id, teamId: team.id });
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Profile URL is ambiguous',
if (!team?.profile?.enabled) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Profile not found',
});
}
if (user?.profile?.enabled) {
let badge: BaseResponse['badge'] = undefined;
if (user.teamMembers[0]) {
badge = {
type: 'Premium',
since: user.teamMembers[0]['createdAt'],
};
}
if (IS_BILLING_ENABLED()) {
const earlyAdopterPriceIds = await getCommunityPlanPriceIds();
const activeEarlyAdopterSub = user.subscriptions.find(
(subscription) =>
subscription.status === SubscriptionStatus.ACTIVE &&
earlyAdopterPriceIds.includes(subscription.priceId),
);
if (activeEarlyAdopterSub) {
badge = {
type: 'EarlySupporter',
since: activeEarlyAdopterSub.createdAt,
};
}
}
return {
type: 'User',
badge,
profile: user.profile,
url: profileUrl,
avatarImageId: user.avatarImageId,
name: user.name || '',
templates: user.templates.filter(
(template): template is PublicDirectLinkTemplate =>
template.directLink?.enabled === true && template.type === TemplateType.PUBLIC,
),
};
}
if (team?.profile?.enabled) {
return {
type: 'Team',
badge: {
type: 'Premium',
since: team.createdAt,
},
profile: team.profile,
url: profileUrl,
avatarImageId: team.avatarImageId,
name: team.name || '',
templates: team.templates.filter(
(template): template is PublicDirectLinkTemplate =>
template.directLink?.enabled === true && template.type === TemplateType.PUBLIC,
),
};
}
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Profile not found',
});
return {
badge: {
type: 'Premium',
since: team.createdAt,
},
profile: team.profile,
url: profileUrl,
avatarImageId: team.avatarImageId,
name: team.name || '',
templates: team.templates.filter(
(template): template is PublicDirectLinkTemplate =>
template.directLink?.enabled === true && template.type === TemplateType.PUBLIC,
),
};
};

View File

@ -2,51 +2,49 @@ import sharp from 'sharp';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/organisations';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
import { AppError } from '../../errors/app-error';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import { buildOrganisationWhereQuery } from '../../utils/organisations';
import { buildTeamWhereQuery } from '../../utils/teams';
export type SetAvatarImageOptions = {
userId: number;
teamId?: number | null;
target:
| {
type: 'user';
}
| {
type: 'team';
teamId: number;
}
| {
type: 'organisation';
organisationId: string;
};
bytes?: string | null;
requestMetadata: ApiRequestMetadata;
};
/**
* Pretty nasty but will do for now.
*/
export const setAvatarImage = async ({
userId,
teamId,
target,
bytes,
requestMetadata,
}: SetAvatarImageOptions) => {
let oldAvatarImageId: string | null = null;
const user = await prisma.user.findUnique({
where: {
id: userId,
},
include: {
avatarImage: true,
},
});
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
oldAvatarImageId = user.avatarImageId;
if (teamId) {
if (target.type === 'team') {
const team = await prisma.team.findFirst({
where: {
id: teamId,
members: {
some: {
userId,
},
},
},
where: buildTeamWhereQuery({
teamId: target.teamId,
userId,
roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
}),
});
if (!team) {
@ -56,6 +54,39 @@ export const setAvatarImage = async ({
}
oldAvatarImageId = team.avatarImageId;
} else if (target.type === 'organisation') {
const organisation = await prisma.organisation.findFirst({
where: buildOrganisationWhereQuery({
organisationId: target.organisationId,
userId,
roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
}),
});
if (!organisation) {
throw new AppError('ORGANISATION_NOT_FOUND', {
statusCode: 404,
});
}
oldAvatarImageId = organisation.avatarImageId;
} else {
const user = await prisma.user.findUnique({
where: {
id: userId,
},
include: {
avatarImage: true,
},
});
if (!user) {
throw new AppError('USER_NOT_FOUND', {
statusCode: 404,
});
}
oldAvatarImageId = user.avatarImageId;
}
if (oldAvatarImageId) {
@ -83,17 +114,26 @@ export const setAvatarImage = async ({
newAvatarImageId = avatarImage.id;
}
if (teamId) {
// TODO: Audit Logs
if (target.type === 'team') {
await prisma.team.update({
where: {
id: teamId,
id: target.teamId,
},
data: {
avatarImageId: newAvatarImageId,
},
});
} else if (target.type === 'organisation') {
await prisma.organisation.update({
where: {
id: target.organisationId,
},
data: {
avatarImageId: newAvatarImageId,
},
});
// TODO: Audit Logs
} else {
await prisma.user.update({
where: {
@ -103,8 +143,6 @@ export const setAvatarImage = async ({
avatarImageId: newAvatarImageId,
},
});
// TODO: Audit Logs
}
return newAvatarImageId;