2025-01-21 22:50:16 +00:00
|
|
|
'use server';
|
|
|
|
|
|
2025-01-23 18:26:41 +00:00
|
|
|
import Ajv from "ajv";
|
2025-02-13 00:48:13 +00:00
|
|
|
import { auth } from "./auth";
|
|
|
|
|
import { notAuthenticated, notFound, ServiceError, unexpectedError, orgInvalidSubscription } from "@/lib/serviceError";
|
2025-01-21 22:50:16 +00:00
|
|
|
import { prisma } from "@/prisma";
|
2025-01-23 18:26:41 +00:00
|
|
|
import { StatusCodes } from "http-status-codes";
|
2025-01-28 18:39:59 +00:00
|
|
|
import { ErrorCode } from "@/lib/errorCodes";
|
|
|
|
|
import { isServiceError } from "@/lib/utils";
|
2025-01-24 18:51:49 +00:00
|
|
|
import { githubSchema } from "@sourcebot/schemas/v3/github.schema";
|
2025-02-04 20:04:05 +00:00
|
|
|
import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema";
|
2025-02-14 18:58:53 +00:00
|
|
|
import { giteaSchema } from "@sourcebot/schemas/v3/gitea.schema";
|
|
|
|
|
import { gerritSchema } from "@sourcebot/schemas/v3/gerrit.schema";
|
2025-02-16 00:37:50 +00:00
|
|
|
import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig, GerritConnectionConfig, ConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
|
2025-01-27 22:07:07 +00:00
|
|
|
import { encrypt } from "@sourcebot/crypto"
|
2025-02-15 18:00:44 +00:00
|
|
|
import { getConnection, getLinkedRepos } from "./data/connection";
|
|
|
|
|
import { ConnectionSyncStatus, Prisma, Invite, OrgRole, Connection, Repo, Org } from "@sourcebot/db";
|
2025-02-12 01:27:02 +00:00
|
|
|
import { headers } from "next/headers"
|
2025-02-14 00:42:33 +00:00
|
|
|
import { getStripe } from "@/lib/stripe"
|
2025-02-12 01:27:02 +00:00
|
|
|
import { getUser } from "@/data/user";
|
2025-02-12 21:05:44 +00:00
|
|
|
import { Session } from "next-auth";
|
2025-02-16 00:37:50 +00:00
|
|
|
import { STRIPE_PRODUCT_ID, CONFIG_MAX_REPOS_NO_TOKEN } from "@/lib/environment";
|
2025-02-15 17:58:17 +00:00
|
|
|
import { StripeSubscriptionStatus } from "@sourcebot/db";
|
2025-02-14 00:20:01 +00:00
|
|
|
import Stripe from "stripe";
|
2025-01-23 18:26:41 +00:00
|
|
|
const ajv = new Ajv({
|
|
|
|
|
validateFormats: false,
|
|
|
|
|
});
|
2025-01-21 22:50:16 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
export const withAuth = async <T>(fn: (session: Session) => Promise<T>) => {
|
2025-01-21 22:50:16 +00:00
|
|
|
const session = await auth();
|
|
|
|
|
if (!session) {
|
|
|
|
|
return notAuthenticated();
|
|
|
|
|
}
|
2025-02-12 21:05:44 +00:00
|
|
|
return fn(session);
|
|
|
|
|
}
|
2025-01-21 22:50:16 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
export const withOrgMembership = async <T>(session: Session, domain: string, fn: (orgId: number) => Promise<T>) => {
|
|
|
|
|
const org = await prisma.org.findUnique({
|
2025-02-12 01:27:02 +00:00
|
|
|
where: {
|
|
|
|
|
domain,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
if (!org) {
|
|
|
|
|
return notFound();
|
2025-01-21 22:50:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const membership = await prisma.userToOrg.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
orgId_userId: {
|
|
|
|
|
userId: session.user.id,
|
2025-02-12 21:05:44 +00:00
|
|
|
orgId: org.id,
|
2025-01-21 22:50:16 +00:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-02-12 21:05:44 +00:00
|
|
|
|
2025-01-21 22:50:16 +00:00
|
|
|
if (!membership) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
return fn(org.id);
|
2025-01-23 18:26:41 +00:00
|
|
|
}
|
|
|
|
|
|
2025-02-14 17:25:22 +00:00
|
|
|
export const withOwner = async <T>(session: Session, domain: string, fn: (orgId: number) => Promise<T>) => {
|
|
|
|
|
const org = await prisma.org.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
domain,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!org) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const userRole = await prisma.userToOrg.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
orgId_userId: {
|
|
|
|
|
orgId: org.id,
|
|
|
|
|
userId: session.user.id,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-02-16 00:37:50 +00:00
|
|
|
if (!userRole || userRole.role !== OrgRole.OWNER) {
|
2025-02-14 17:25:22 +00:00
|
|
|
return {
|
|
|
|
|
statusCode: StatusCodes.FORBIDDEN,
|
|
|
|
|
errorCode: ErrorCode.MEMBER_NOT_OWNER,
|
|
|
|
|
message: "Only org owners can perform this action",
|
|
|
|
|
} satisfies ServiceError;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fn(org.id);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-12 22:55:35 +00:00
|
|
|
export const isAuthed = async () => {
|
|
|
|
|
const session = await auth();
|
|
|
|
|
return session != null;
|
|
|
|
|
}
|
2025-01-23 18:26:41 +00:00
|
|
|
|
2025-02-12 22:55:35 +00:00
|
|
|
export const createOrg = (name: string, domain: string, stripeCustomerId?: string): Promise<{ id: number } | ServiceError> =>
|
2025-02-12 21:05:44 +00:00
|
|
|
withAuth(async (session) => {
|
|
|
|
|
const org = await prisma.org.create({
|
|
|
|
|
data: {
|
|
|
|
|
name,
|
|
|
|
|
domain,
|
2025-02-12 22:55:35 +00:00
|
|
|
stripeCustomerId,
|
2025-02-15 17:58:17 +00:00
|
|
|
stripeSubscriptionStatus: StripeSubscriptionStatus.ACTIVE,
|
|
|
|
|
stripeLastUpdatedAt: new Date(),
|
2025-02-12 21:05:44 +00:00
|
|
|
members: {
|
|
|
|
|
create: {
|
|
|
|
|
role: "OWNER",
|
|
|
|
|
user: {
|
|
|
|
|
connect: {
|
|
|
|
|
id: session.user.id,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-02-04 20:04:05 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
return {
|
|
|
|
|
id: org.id,
|
2025-02-04 20:04:05 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
export const getSecrets = (domain: string): Promise<{ createdAt: Date; key: string; }[] | ServiceError> =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
const secrets = await prisma.secret.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
orgId,
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
key: true,
|
|
|
|
|
createdAt: true
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return secrets.map((secret) => ({
|
|
|
|
|
key: secret.key,
|
|
|
|
|
createdAt: secret.createdAt,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
export const createSecret = async (key: string, value: string, domain: string): Promise<{ success: boolean } | ServiceError> =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
try {
|
|
|
|
|
const encrypted = encrypt(value);
|
|
|
|
|
await prisma.secret.create({
|
|
|
|
|
data: {
|
|
|
|
|
orgId,
|
|
|
|
|
key,
|
|
|
|
|
encryptedValue: encrypted.encryptedData,
|
|
|
|
|
iv: encrypted.iv,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} catch {
|
|
|
|
|
return unexpectedError(`Failed to create secret`);
|
|
|
|
|
}
|
2025-02-04 20:04:05 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
export const deleteSecret = async (key: string, domain: string): Promise<{ success: boolean } | ServiceError> =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
await prisma.secret.delete({
|
|
|
|
|
where: {
|
|
|
|
|
orgId_key: {
|
|
|
|
|
orgId,
|
|
|
|
|
key,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-02-04 20:04:05 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getConnections = async (domain: string): Promise<
|
|
|
|
|
{
|
|
|
|
|
id: number,
|
|
|
|
|
name: string,
|
|
|
|
|
syncStatus: ConnectionSyncStatus,
|
|
|
|
|
connectionType: string,
|
|
|
|
|
updatedAt: Date,
|
|
|
|
|
syncedAt?: Date
|
|
|
|
|
}[] | ServiceError
|
|
|
|
|
> =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
const connections = await prisma.connection.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
orgId,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return connections.map((connection) => ({
|
|
|
|
|
id: connection.id,
|
|
|
|
|
name: connection.name,
|
|
|
|
|
syncStatus: connection.syncStatus,
|
|
|
|
|
connectionType: connection.connectionType,
|
|
|
|
|
updatedAt: connection.updatedAt,
|
|
|
|
|
syncedAt: connection.syncedAt ?? undefined,
|
|
|
|
|
}));
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const createConnection = async (name: string, type: string, connectionConfig: string, domain: string): Promise<{ id: number } | ServiceError> =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
const parsedConfig = parseConnectionConfig(type, connectionConfig);
|
|
|
|
|
if (isServiceError(parsedConfig)) {
|
|
|
|
|
return parsedConfig;
|
|
|
|
|
}
|
2025-02-04 20:04:05 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
const connection = await prisma.connection.create({
|
|
|
|
|
data: {
|
|
|
|
|
orgId,
|
|
|
|
|
name,
|
|
|
|
|
config: parsedConfig as unknown as Prisma.InputJsonValue,
|
|
|
|
|
connectionType: type,
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-02-04 20:04:05 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
return {
|
|
|
|
|
id: connection.id,
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
2025-02-15 18:00:44 +00:00
|
|
|
export const getConnectionInfoAction = async (connectionId: number, domain: string): Promise<{ connection: Connection, linkedRepos: Repo[] } | ServiceError> =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
const connection = await getConnection(connectionId, orgId);
|
|
|
|
|
if (!connection) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const linkedRepos = await getLinkedRepos(connectionId, orgId);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
connection,
|
|
|
|
|
linkedRepos: linkedRepos.map((repo) => repo.repo),
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const getOrgFromDomainAction = async (domain: string): Promise<Org | ServiceError> =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
const org = await prisma.org.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
id: orgId,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!org) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return org;
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
export const updateConnectionDisplayName = async (connectionId: number, name: string, domain: string): Promise<{ success: boolean } | ServiceError> =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
const connection = await getConnection(connectionId, orgId);
|
|
|
|
|
if (!connection) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
2025-02-04 20:04:05 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
await prisma.connection.update({
|
|
|
|
|
where: {
|
|
|
|
|
id: connectionId,
|
|
|
|
|
orgId,
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
name,
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-02-04 20:04:05 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
export const updateConnectionConfigAndScheduleSync = async (connectionId: number, config: string, domain: string): Promise<{ success: boolean } | ServiceError> =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
const connection = await getConnection(connectionId, orgId);
|
|
|
|
|
if (!connection) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
2025-02-04 20:04:05 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
const parsedConfig = parseConnectionConfig(connection.connectionType, config);
|
|
|
|
|
if (isServiceError(parsedConfig)) {
|
|
|
|
|
return parsedConfig;
|
|
|
|
|
}
|
2025-02-04 20:04:05 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
if (connection.syncStatus === "SYNC_NEEDED" ||
|
|
|
|
|
connection.syncStatus === "IN_SYNC_QUEUE" ||
|
|
|
|
|
connection.syncStatus === "SYNCING") {
|
|
|
|
|
return {
|
|
|
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
|
|
|
errorCode: ErrorCode.CONNECTION_SYNC_ALREADY_SCHEDULED,
|
|
|
|
|
message: "Connection is already syncing. Please wait for the sync to complete before updating the connection.",
|
|
|
|
|
} satisfies ServiceError;
|
|
|
|
|
}
|
2025-02-04 20:04:05 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
await prisma.connection.update({
|
|
|
|
|
where: {
|
|
|
|
|
id: connectionId,
|
|
|
|
|
orgId,
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
config: parsedConfig as unknown as Prisma.InputJsonValue,
|
|
|
|
|
syncStatus: "SYNC_NEEDED",
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-02-04 20:04:05 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
2025-02-15 18:00:44 +00:00
|
|
|
export const flagConnectionForSync = async (connectionId: number, domain: string): Promise<{ success: boolean } | ServiceError> =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
const connection = await getConnection(connectionId, orgId);
|
|
|
|
|
if (!connection || connection.orgId !== orgId) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (connection.syncStatus !== "FAILED") {
|
|
|
|
|
return {
|
|
|
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
|
|
|
errorCode: ErrorCode.CONNECTION_NOT_FAILED,
|
|
|
|
|
message: "Connection is not in a failed state. Cannot flag for sync.",
|
|
|
|
|
} satisfies ServiceError;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await prisma.connection.update({
|
2025-02-16 00:37:50 +00:00
|
|
|
where: {
|
2025-02-15 18:00:44 +00:00
|
|
|
id: connection.id,
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
syncStatus: "SYNC_NEEDED",
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
export const deleteConnection = async (connectionId: number, domain: string): Promise<{ success: boolean } | ServiceError> =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
const connection = await getConnection(connectionId, orgId);
|
|
|
|
|
if (!connection) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
2025-02-04 20:04:05 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
await prisma.connection.delete({
|
|
|
|
|
where: {
|
|
|
|
|
id: connectionId,
|
|
|
|
|
orgId,
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-02-04 20:04:05 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
2025-02-14 17:25:22 +00:00
|
|
|
export const getCurrentUserRole = async (domain: string): Promise<OrgRole | ServiceError> =>
|
2025-02-12 21:05:44 +00:00
|
|
|
withAuth((session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
2025-02-14 17:25:22 +00:00
|
|
|
const userRole = await prisma.userToOrg.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
orgId_userId: {
|
|
|
|
|
orgId,
|
|
|
|
|
userId: session.user.id,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!userRole) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return userRole.role;
|
2025-02-16 00:37:50 +00:00
|
|
|
})
|
2025-02-14 17:25:22 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const createInvite = async (email: string, userId: string, domain: string): Promise<{ success: boolean } | ServiceError> =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOwner(session, domain, async (orgId) => {
|
2025-02-12 21:05:44 +00:00
|
|
|
console.log("Creating invite for", email, userId, orgId);
|
|
|
|
|
|
2025-02-13 03:50:44 +00:00
|
|
|
if (email === session.user.email) {
|
|
|
|
|
console.error("User tried to invite themselves");
|
|
|
|
|
return {
|
|
|
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
|
|
|
errorCode: ErrorCode.SELF_INVITE,
|
|
|
|
|
message: "❌ You can't invite yourself to an org",
|
|
|
|
|
} satisfies ServiceError;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
try {
|
|
|
|
|
await prisma.invite.create({
|
|
|
|
|
data: {
|
|
|
|
|
recipientEmail: email,
|
|
|
|
|
hostUserId: userId,
|
|
|
|
|
orgId,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to create invite:", error);
|
|
|
|
|
return unexpectedError("Failed to create invite");
|
|
|
|
|
}
|
2025-02-04 20:04:05 +00:00
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const redeemInvite = async (invite: Invite, userId: string): Promise<{ success: boolean } | ServiceError> =>
|
|
|
|
|
withAuth(async () => {
|
|
|
|
|
try {
|
2025-02-13 23:21:31 +00:00
|
|
|
const res = await prisma.$transaction(async (tx) => {
|
2025-02-13 00:48:13 +00:00
|
|
|
const org = await tx.org.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
id: invite.orgId,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!org) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-13 23:11:48 +00:00
|
|
|
// Incrememnt the seat count
|
2025-02-13 00:48:13 +00:00
|
|
|
if (org.stripeCustomerId) {
|
2025-02-13 23:11:48 +00:00
|
|
|
const subscription = await fetchSubscription(org.domain);
|
2025-02-13 00:48:13 +00:00
|
|
|
if (isServiceError(subscription)) {
|
2025-02-13 23:36:48 +00:00
|
|
|
throw orgInvalidSubscription();
|
2025-02-13 00:48:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const existingSeatCount = subscription.items.data[0].quantity;
|
|
|
|
|
const newSeatCount = (existingSeatCount || 1) + 1
|
|
|
|
|
|
2025-02-14 00:42:33 +00:00
|
|
|
const stripe = getStripe();
|
2025-02-13 00:48:13 +00:00
|
|
|
await stripe.subscriptionItems.update(
|
|
|
|
|
subscription.items.data[0].id,
|
|
|
|
|
{
|
|
|
|
|
quantity: newSeatCount,
|
|
|
|
|
proration_behavior: 'create_prorations',
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
await tx.userToOrg.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId,
|
|
|
|
|
orgId: invite.orgId,
|
|
|
|
|
role: "MEMBER",
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await tx.invite.delete({
|
|
|
|
|
where: {
|
|
|
|
|
id: invite.id,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-02-13 23:21:31 +00:00
|
|
|
if (isServiceError(res)) {
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-12 21:05:44 +00:00
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to redeem invite:", error);
|
|
|
|
|
return unexpectedError("Failed to redeem invite");
|
2025-02-04 20:04:05 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-02-14 17:25:22 +00:00
|
|
|
export const makeOwner = async (newOwnerId: string, domain: string): Promise<{ success: boolean } | ServiceError> =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOwner(session, domain, async (orgId) => {
|
|
|
|
|
const currentUserId = session.user.id;
|
|
|
|
|
const currentUserRole = await prisma.userToOrg.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
orgId_userId: {
|
|
|
|
|
userId: currentUserId,
|
|
|
|
|
orgId,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (newOwnerId === currentUserId) {
|
|
|
|
|
return {
|
|
|
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
|
|
|
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
|
|
|
|
message: "You're already the owner of this org",
|
|
|
|
|
} satisfies ServiceError;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newOwner = await prisma.userToOrg.findUnique({
|
2025-02-16 00:37:50 +00:00
|
|
|
where: {
|
2025-02-14 17:25:22 +00:00
|
|
|
orgId_userId: {
|
|
|
|
|
userId: newOwnerId,
|
|
|
|
|
orgId,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!newOwner) {
|
|
|
|
|
return {
|
|
|
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
|
|
|
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
|
|
|
|
message: "The user you're trying to make the owner doesn't exist",
|
|
|
|
|
} satisfies ServiceError;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await prisma.$transaction([
|
|
|
|
|
prisma.userToOrg.update({
|
|
|
|
|
where: {
|
|
|
|
|
orgId_userId: {
|
|
|
|
|
userId: newOwnerId,
|
|
|
|
|
orgId,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
role: "OWNER",
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
prisma.userToOrg.update({
|
|
|
|
|
where: {
|
|
|
|
|
orgId_userId: {
|
|
|
|
|
userId: currentUserId,
|
|
|
|
|
orgId,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
role: "MEMBER",
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
2025-02-04 20:04:05 +00:00
|
|
|
const parseConnectionConfig = (connectionType: string, config: string) => {
|
|
|
|
|
let parsedConfig: ConnectionConfig;
|
2025-01-23 18:26:41 +00:00
|
|
|
try {
|
|
|
|
|
parsedConfig = JSON.parse(config);
|
2025-02-04 20:04:05 +00:00
|
|
|
} catch (_e) {
|
2025-01-23 18:26:41 +00:00
|
|
|
return {
|
|
|
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
|
|
|
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
|
|
|
|
message: "config must be a valid JSON object."
|
|
|
|
|
} satisfies ServiceError;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-04 20:04:05 +00:00
|
|
|
const schema = (() => {
|
|
|
|
|
switch (connectionType) {
|
|
|
|
|
case "github":
|
|
|
|
|
return githubSchema;
|
|
|
|
|
case "gitlab":
|
|
|
|
|
return gitlabSchema;
|
2025-02-14 18:58:53 +00:00
|
|
|
case 'gitea':
|
|
|
|
|
return giteaSchema;
|
|
|
|
|
case 'gerrit':
|
|
|
|
|
return gerritSchema;
|
2025-02-04 20:04:05 +00:00
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
if (!schema) {
|
|
|
|
|
return {
|
|
|
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
|
|
|
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
|
|
|
|
message: "invalid connection type",
|
|
|
|
|
} satisfies ServiceError;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-16 00:37:50 +00:00
|
|
|
const { numRepos, hasToken } = (() => {
|
|
|
|
|
switch (connectionType) {
|
|
|
|
|
case "github":
|
|
|
|
|
const githubConfig = parsedConfig as GithubConnectionConfig;
|
|
|
|
|
return {
|
|
|
|
|
numRepos: githubConfig.repos?.length,
|
|
|
|
|
hasToken: !!githubConfig.token,
|
|
|
|
|
}
|
|
|
|
|
case "gitlab":
|
|
|
|
|
const gitlabConfig = parsedConfig as GitlabConnectionConfig;
|
|
|
|
|
return {
|
|
|
|
|
numRepos: gitlabConfig.projects?.length,
|
|
|
|
|
hasToken: !!gitlabConfig.token,
|
|
|
|
|
}
|
|
|
|
|
case "gitea":
|
|
|
|
|
const giteaConfig = parsedConfig as GiteaConnectionConfig;
|
|
|
|
|
return {
|
|
|
|
|
numRepos: giteaConfig.repos?.length,
|
|
|
|
|
hasToken: !!giteaConfig.token,
|
|
|
|
|
}
|
|
|
|
|
case "gerrit":
|
|
|
|
|
const gerritConfig = parsedConfig as GerritConnectionConfig;
|
|
|
|
|
return {
|
|
|
|
|
numRepos: gerritConfig.projects?.length,
|
|
|
|
|
hasToken: true, // gerrit doesn't use a token atm
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return {
|
|
|
|
|
numRepos: undefined,
|
|
|
|
|
hasToken: true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
if (!hasToken && numRepos && numRepos > CONFIG_MAX_REPOS_NO_TOKEN) {
|
|
|
|
|
return {
|
|
|
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
|
|
|
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
|
|
|
|
message: `You must provide a token to sync more than ${CONFIG_MAX_REPOS_NO_TOKEN} repositories.`,
|
|
|
|
|
} satisfies ServiceError;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-04 20:04:05 +00:00
|
|
|
const isValidConfig = ajv.validate(schema, parsedConfig);
|
2025-01-23 18:26:41 +00:00
|
|
|
if (!isValidConfig) {
|
|
|
|
|
return {
|
|
|
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
|
|
|
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
|
|
|
|
message: `config schema validation failed with errors: ${ajv.errorsText(ajv.errors)}`,
|
|
|
|
|
} satisfies ServiceError;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-04 20:04:05 +00:00
|
|
|
return parsedConfig;
|
2025-01-31 19:15:54 +00:00
|
|
|
}
|
2025-02-10 22:31:38 +00:00
|
|
|
|
2025-02-13 18:24:12 +00:00
|
|
|
export const setupInitialStripeCustomer = async (name: string, domain: string) =>
|
|
|
|
|
withAuth(async (session) => {
|
|
|
|
|
const user = await getUser(session.user.id);
|
|
|
|
|
if (!user) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
2025-02-12 01:27:02 +00:00
|
|
|
|
2025-02-14 00:42:33 +00:00
|
|
|
const stripe = getStripe();
|
2025-02-13 18:24:12 +00:00
|
|
|
const origin = (await headers()).get('origin')
|
2025-02-11 03:26:42 +00:00
|
|
|
|
2025-02-13 23:21:31 +00:00
|
|
|
// @nocheckin
|
2025-02-13 18:24:12 +00:00
|
|
|
const test_clock = await stripe.testHelpers.testClocks.create({
|
|
|
|
|
frozen_time: Math.floor(Date.now() / 1000)
|
|
|
|
|
})
|
2025-02-12 21:03:31 +00:00
|
|
|
|
2025-02-13 18:24:12 +00:00
|
|
|
const customer = await stripe.customers.create({
|
|
|
|
|
name: user.name!,
|
|
|
|
|
email: user.email!,
|
|
|
|
|
test_clock: test_clock.id
|
|
|
|
|
})
|
2025-02-12 21:03:31 +00:00
|
|
|
|
2025-02-13 18:24:12 +00:00
|
|
|
const prices = await stripe.prices.list({
|
|
|
|
|
product: STRIPE_PRODUCT_ID,
|
|
|
|
|
expand: ['data.product'],
|
|
|
|
|
});
|
|
|
|
|
const stripeSession = await stripe.checkout.sessions.create({
|
|
|
|
|
ui_mode: 'embedded',
|
|
|
|
|
customer: customer.id,
|
|
|
|
|
line_items: [
|
|
|
|
|
{
|
|
|
|
|
price: prices.data[0].id,
|
|
|
|
|
quantity: 1
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
mode: 'subscription',
|
|
|
|
|
subscription_data: {
|
|
|
|
|
trial_period_days: 7,
|
|
|
|
|
trial_settings: {
|
|
|
|
|
end_behavior: {
|
|
|
|
|
missing_payment_method: 'cancel',
|
|
|
|
|
},
|
2025-02-13 00:48:13 +00:00
|
|
|
},
|
|
|
|
|
},
|
2025-02-13 18:24:12 +00:00
|
|
|
payment_method_collection: 'if_required',
|
|
|
|
|
return_url: `${origin}/onboard/complete?session_id={CHECKOUT_SESSION_ID}&org_name=${name}&org_domain=${domain}`,
|
|
|
|
|
})
|
2025-02-12 01:27:02 +00:00
|
|
|
|
2025-02-13 18:24:12 +00:00
|
|
|
return stripeSession.client_secret!;
|
|
|
|
|
});
|
2025-02-11 03:26:42 +00:00
|
|
|
|
2025-02-13 00:48:13 +00:00
|
|
|
export const getSubscriptionCheckoutRedirect = async (domain: string) =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
const org = await prisma.org.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
id: orgId,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-02-12 21:03:31 +00:00
|
|
|
|
2025-02-13 00:48:13 +00:00
|
|
|
if (!org || !org.stripeCustomerId) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
2025-02-12 21:03:31 +00:00
|
|
|
|
2025-02-13 00:48:13 +00:00
|
|
|
const orgMembers = await prisma.userToOrg.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
orgId,
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
userId: true,
|
2025-02-12 21:03:31 +00:00
|
|
|
}
|
2025-02-13 00:48:13 +00:00
|
|
|
});
|
|
|
|
|
const numOrgMembers = orgMembers.length;
|
2025-02-12 21:03:31 +00:00
|
|
|
|
2025-02-14 00:42:33 +00:00
|
|
|
const stripe = getStripe();
|
2025-02-13 00:48:13 +00:00
|
|
|
const origin = (await headers()).get('origin')
|
|
|
|
|
const prices = await stripe.prices.list({
|
2025-02-13 18:24:12 +00:00
|
|
|
product: STRIPE_PRODUCT_ID,
|
2025-02-13 00:48:13 +00:00
|
|
|
expand: ['data.product'],
|
|
|
|
|
});
|
2025-02-12 21:03:31 +00:00
|
|
|
|
2025-02-13 00:48:13 +00:00
|
|
|
const createNewSubscription = async () => {
|
|
|
|
|
const stripeSession = await stripe.checkout.sessions.create({
|
|
|
|
|
customer: org.stripeCustomerId as string,
|
|
|
|
|
payment_method_types: ['card'],
|
|
|
|
|
line_items: [
|
|
|
|
|
{
|
|
|
|
|
price: prices.data[0].id,
|
|
|
|
|
quantity: numOrgMembers
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
mode: 'subscription',
|
|
|
|
|
payment_method_collection: 'always',
|
|
|
|
|
success_url: `${origin}/${domain}/settings/billing`,
|
|
|
|
|
cancel_url: `${origin}/${domain}`,
|
|
|
|
|
});
|
2025-02-12 21:03:31 +00:00
|
|
|
|
2025-02-13 00:48:13 +00:00
|
|
|
return stripeSession.url;
|
|
|
|
|
}
|
2025-02-12 21:03:31 +00:00
|
|
|
|
2025-02-13 00:48:13 +00:00
|
|
|
const newSubscriptionUrl = await createNewSubscription();
|
|
|
|
|
return newSubscriptionUrl;
|
|
|
|
|
})
|
|
|
|
|
)
|
2025-02-12 21:03:31 +00:00
|
|
|
|
2025-02-12 01:27:02 +00:00
|
|
|
export async function fetchStripeSession(sessionId: string) {
|
2025-02-14 00:42:33 +00:00
|
|
|
const stripe = getStripe();
|
2025-02-12 01:27:02 +00:00
|
|
|
const stripeSession = await stripe.checkout.sessions.retrieve(sessionId);
|
|
|
|
|
return stripeSession;
|
2025-02-12 03:06:40 +00:00
|
|
|
}
|
|
|
|
|
|
2025-02-12 22:55:35 +00:00
|
|
|
export const getCustomerPortalSessionLink = async (domain: string): Promise<string | ServiceError> =>
|
|
|
|
|
withAuth((session) =>
|
2025-02-14 17:25:22 +00:00
|
|
|
withOwner(session, domain, async (orgId) => {
|
2025-02-12 22:55:35 +00:00
|
|
|
const org = await prisma.org.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
id: orgId,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-02-12 03:06:40 +00:00
|
|
|
|
2025-02-12 22:55:35 +00:00
|
|
|
if (!org || !org.stripeCustomerId) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
2025-02-12 03:06:40 +00:00
|
|
|
|
2025-02-14 00:42:33 +00:00
|
|
|
const stripe = getStripe();
|
2025-02-12 22:55:35 +00:00
|
|
|
const origin = (await headers()).get('origin')
|
|
|
|
|
const portalSession = await stripe.billingPortal.sessions.create({
|
|
|
|
|
customer: org.stripeCustomerId as string,
|
|
|
|
|
return_url: `${origin}/${domain}/settings/billing`,
|
|
|
|
|
});
|
2025-02-12 03:06:40 +00:00
|
|
|
|
2025-02-12 22:55:35 +00:00
|
|
|
return portalSession.url;
|
|
|
|
|
}));
|
2025-02-12 21:03:31 +00:00
|
|
|
|
2025-02-14 00:20:01 +00:00
|
|
|
export const fetchSubscription = (domain: string): Promise<Stripe.Subscription | ServiceError> =>
|
2025-02-13 23:36:48 +00:00
|
|
|
withAuth(async () => {
|
|
|
|
|
const org = await prisma.org.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
domain,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-02-12 21:03:31 +00:00
|
|
|
|
2025-02-13 23:36:48 +00:00
|
|
|
if (!org || !org.stripeCustomerId) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
2025-02-12 21:03:31 +00:00
|
|
|
|
2025-02-14 00:42:33 +00:00
|
|
|
const stripe = getStripe();
|
2025-02-13 23:36:48 +00:00
|
|
|
const subscriptions = await stripe.subscriptions.list({
|
|
|
|
|
customer: org.stripeCustomerId
|
|
|
|
|
});
|
2025-02-12 21:03:31 +00:00
|
|
|
|
2025-02-13 23:36:48 +00:00
|
|
|
if (subscriptions.data.length === 0) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
|
|
|
|
return subscriptions.data[0];
|
|
|
|
|
});
|
2025-02-12 22:55:35 +00:00
|
|
|
|
2025-02-14 17:25:22 +00:00
|
|
|
export const getSubscriptionBillingEmail = async (domain: string): Promise<string | ServiceError> =>
|
|
|
|
|
withAuth(async (session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
const org = await prisma.org.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
id: orgId,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!org || !org.stripeCustomerId) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const stripe = getStripe();
|
|
|
|
|
const customer = await stripe.customers.retrieve(org.stripeCustomerId);
|
|
|
|
|
if (!('email' in customer) || customer.deleted) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
|
|
|
|
return customer.email!;
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const changeSubscriptionBillingEmail = async (domain: string, newEmail: string): Promise<{ success: boolean } | ServiceError> =>
|
|
|
|
|
withAuth((session) =>
|
|
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
const userRole = await prisma.userToOrg.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
orgId_userId: {
|
|
|
|
|
orgId,
|
|
|
|
|
userId: session.user.id,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!userRole || userRole.role !== "OWNER") {
|
|
|
|
|
return {
|
|
|
|
|
statusCode: StatusCodes.FORBIDDEN,
|
|
|
|
|
errorCode: ErrorCode.MEMBER_NOT_OWNER,
|
|
|
|
|
message: "Only org owners can change billing email",
|
|
|
|
|
} satisfies ServiceError;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const org = await prisma.org.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
id: orgId,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!org || !org.stripeCustomerId) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const stripe = getStripe();
|
|
|
|
|
await stripe.customers.update(org.stripeCustomerId, {
|
|
|
|
|
email: newEmail,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
2025-02-13 18:15:06 +00:00
|
|
|
export const checkIfUserHasOrg = async (userId: string): Promise<boolean | ServiceError> => {
|
|
|
|
|
const orgs = await prisma.userToOrg.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
userId,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return orgs.length > 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-12 22:55:35 +00:00
|
|
|
export const checkIfOrgDomainExists = async (domain: string): Promise<boolean | ServiceError> =>
|
2025-02-13 18:15:06 +00:00
|
|
|
withAuth(async () => {
|
2025-02-12 22:55:35 +00:00
|
|
|
const org = await prisma.org.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
domain,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return !!org;
|
|
|
|
|
});
|
2025-02-13 18:15:06 +00:00
|
|
|
|
|
|
|
|
export const removeMember = async (memberId: string, domain: string): Promise<{ success: boolean } | ServiceError> =>
|
2025-02-13 23:36:48 +00:00
|
|
|
withAuth(async (session) =>
|
2025-02-13 18:15:06 +00:00
|
|
|
withOrgMembership(session, domain, async (orgId) => {
|
|
|
|
|
const targetMember = await prisma.userToOrg.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
orgId_userId: {
|
|
|
|
|
orgId,
|
|
|
|
|
userId: memberId,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!targetMember) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const org = await prisma.org.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
id: orgId,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!org) {
|
|
|
|
|
return notFound();
|
|
|
|
|
}
|
2025-02-13 23:36:48 +00:00
|
|
|
|
2025-02-13 18:15:06 +00:00
|
|
|
if (org.stripeCustomerId) {
|
2025-02-13 23:11:48 +00:00
|
|
|
const subscription = await fetchSubscription(domain);
|
2025-02-13 18:15:06 +00:00
|
|
|
if (isServiceError(subscription)) {
|
|
|
|
|
return orgInvalidSubscription();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const existingSeatCount = subscription.items.data[0].quantity;
|
|
|
|
|
const newSeatCount = (existingSeatCount || 1) - 1;
|
|
|
|
|
|
2025-02-14 00:42:33 +00:00
|
|
|
const stripe = getStripe();
|
2025-02-13 18:15:06 +00:00
|
|
|
await stripe.subscriptionItems.update(
|
|
|
|
|
subscription.items.data[0].id,
|
|
|
|
|
{
|
|
|
|
|
quantity: newSeatCount,
|
|
|
|
|
proration_behavior: 'create_prorations',
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await prisma.userToOrg.delete({
|
|
|
|
|
where: {
|
|
|
|
|
orgId_userId: {
|
|
|
|
|
orgId,
|
|
|
|
|
userId: memberId,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
2025-02-13 18:52:34 +00:00
|
|
|
|
2025-02-13 23:36:48 +00:00
|
|
|
export const getSubscriptionData = async (domain: string) =>
|
2025-02-13 18:52:34 +00:00
|
|
|
withAuth(async (session) =>
|
2025-02-14 00:20:01 +00:00
|
|
|
withOrgMembership(session, domain, async () => {
|
2025-02-13 23:11:48 +00:00
|
|
|
const subscription = await fetchSubscription(domain);
|
2025-02-13 18:52:34 +00:00
|
|
|
if (isServiceError(subscription)) {
|
|
|
|
|
return orgInvalidSubscription();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
plan: "Team",
|
|
|
|
|
seats: subscription.items.data[0].quantity!,
|
|
|
|
|
perSeatPrice: subscription.items.data[0].price.unit_amount! / 100,
|
|
|
|
|
nextBillingDate: subscription.current_period_end!,
|
2025-02-13 19:07:04 +00:00
|
|
|
status: subscription.status,
|
2025-02-13 18:52:34 +00:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|