Files
AFFiNE-Mirror/packages/backend/server/src/plugins/payment/license/controller.ts
T
2025-03-27 12:32:28 +00:00

273 lines
6.0 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import {
Body,
Controller,
Get,
Headers,
HttpStatus,
Logger,
Param,
Post,
Res,
} from '@nestjs/common';
import { PrismaClient, Subscription } from '@prisma/client';
import type { Response } from 'express';
import Stripe from 'stripe';
import { z } from 'zod';
import {
CustomerPortalCreateFailed,
InvalidLicenseToActivate,
InvalidLicenseUpdateParams,
LicenseNotFound,
Mutex,
} from '../../../base';
import { Public } from '../../../core/auth';
import { SelfhostTeamSubscriptionManager } from '../manager/selfhost';
import { SubscriptionService } from '../service';
import { StripeFactory } from '../stripe';
import {
SubscriptionPlan,
SubscriptionRecurring,
SubscriptionStatus,
} from '../types';
const UpdateSeatsParams = z.object({
seats: z.number().min(1),
});
const UpdateRecurringParams = z.object({
recurring: z.enum([
SubscriptionRecurring.Monthly,
SubscriptionRecurring.Yearly,
]),
});
@Public()
@Controller('/api/team/licenses')
export class LicenseController {
private readonly logger = new Logger(LicenseController.name);
constructor(
private readonly db: PrismaClient,
private readonly mutex: Mutex,
private readonly subscription: SubscriptionService,
private readonly manager: SelfhostTeamSubscriptionManager,
private readonly stripeProvider: StripeFactory
) {}
@Post('/:license/activate')
async activate(@Res() res: Response, @Param('license') key: string) {
await using lock = await this.mutex.acquire(`license-activation:${key}`);
if (!lock) {
throw new InvalidLicenseToActivate();
}
const license = await this.db.license.findUnique({
where: {
key,
},
});
if (!license) {
throw new InvalidLicenseToActivate();
}
const subscription = await this.manager.getSubscription({
key: license.key,
plan: SubscriptionPlan.SelfHostedTeam,
});
if (
!subscription ||
license.installedAt ||
subscription.status !== SubscriptionStatus.Active
) {
throw new InvalidLicenseToActivate();
}
const validateKey = randomUUID();
await this.db.license.update({
where: {
key,
},
data: {
installedAt: new Date(),
validateKey,
},
});
res
.status(HttpStatus.OK)
.header('x-next-validate-key', validateKey)
.json(this.license(subscription));
}
@Post('/:license/deactivate')
async deactivate(@Param('license') key: string) {
await this.db.license.update({
where: {
key,
},
data: {
installedAt: null,
validateKey: null,
},
});
return {
success: true,
};
}
@Get('/:license/health')
async health(
@Res() res: Response,
@Param('license') key: string,
@Headers('x-validate-key') revalidateKey: string
) {
const license = await this.db.license.findUnique({
where: {
key,
},
});
const subscription = await this.manager.getSubscription({
key,
plan: SubscriptionPlan.SelfHostedTeam,
});
if (!license || !subscription) {
throw new LicenseNotFound();
}
if (license.validateKey && license.validateKey !== revalidateKey) {
throw new InvalidLicenseToActivate();
}
const validateKey = randomUUID();
await this.db.license.update({
where: {
key,
},
data: {
validateKey,
},
});
res
.status(HttpStatus.OK)
.header('x-next-validate-key', validateKey)
.json(this.license(subscription));
}
@Post('/:license/seats')
async updateSeats(
@Param('license') key: string,
@Body() body: z.infer<typeof UpdateSeatsParams>
) {
const parseResult = UpdateSeatsParams.safeParse(body);
if (parseResult.error) {
throw new InvalidLicenseUpdateParams({
reason: parseResult.error.message,
});
}
const license = await this.db.license.findUnique({
where: {
key,
},
});
if (!license) {
throw new LicenseNotFound();
}
await this.subscription.updateSubscriptionQuantity(
{
key: license.key,
plan: SubscriptionPlan.SelfHostedTeam,
},
parseResult.data.seats
);
}
@Post('/:license/recurring')
async updateRecurring(
@Param('license') key: string,
@Body() body: z.infer<typeof UpdateRecurringParams>
) {
const parseResult = UpdateRecurringParams.safeParse(body);
if (parseResult.error) {
throw new InvalidLicenseUpdateParams({
reason: parseResult.error.message,
});
}
const license = await this.db.license.findUnique({
where: {
key,
},
});
if (!license) {
throw new LicenseNotFound();
}
await this.subscription.updateSubscriptionRecurring(
{
key: license.key,
plan: SubscriptionPlan.SelfHostedTeam,
},
parseResult.data.recurring
);
}
@Post('/:license/create-customer-portal')
async createCustomerPortal(@Param('license') key: string) {
const subscription = await this.db.subscription.findFirst({
where: {
targetId: key,
},
});
if (!subscription || !subscription.stripeSubscriptionId) {
throw new LicenseNotFound();
}
const subscriptionData =
await this.stripeProvider.stripe.subscriptions.retrieve(
subscription.stripeSubscriptionId,
{
expand: ['customer'],
}
);
const customer = subscriptionData.customer as Stripe.Customer;
try {
const portal =
await this.stripeProvider.stripe.billingPortal.sessions.create({
customer: customer.id,
});
return { url: portal.url };
} catch (e) {
this.logger.error('Failed to create customer portal.', e);
throw new CustomerPortalCreateFailed();
}
}
license(subscription: Subscription) {
return {
plan: subscription.plan,
recurring: subscription.recurring,
quantity: subscription.quantity,
endAt: subscription.end?.getTime(),
};
}
}