226 lines
7.8 KiB
TypeScript
226 lines
7.8 KiB
TypeScript
import { stripe } from "@better-auth/stripe";
|
|
import { apiKey, genericOAuth } from "better-auth/plugins";
|
|
import { magicLink } from "better-auth/plugins/magic-link";
|
|
|
|
import type { dbClient } from "@kan/db/client";
|
|
import * as memberRepo from "@kan/db/repository/member.repo";
|
|
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
|
|
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
|
import { sendEmail } from "@kan/email";
|
|
import { createStripeClient } from "@kan/stripe";
|
|
|
|
import { socialProvidersPlugin } from "./providers";
|
|
import { triggerWorkflow } from "./utils";
|
|
|
|
export function createPlugins(db: dbClient) {
|
|
return [
|
|
socialProvidersPlugin(),
|
|
...(process.env.NEXT_PUBLIC_KAN_ENV === "cloud"
|
|
? [
|
|
stripe({
|
|
stripeClient: createStripeClient(),
|
|
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
createCustomerOnSignUp: true,
|
|
subscription: {
|
|
enabled: true,
|
|
plans: [
|
|
{
|
|
name: "team",
|
|
priceId: process.env.STRIPE_TEAM_PLAN_MONTHLY_PRICE_ID!,
|
|
annualDiscountPriceId:
|
|
process.env.STRIPE_TEAM_PLAN_YEARLY_PRICE_ID!,
|
|
freeTrial: {
|
|
days: 14,
|
|
onTrialStart: async (subscription) => {
|
|
await triggerWorkflow(db, "trial-start", subscription);
|
|
},
|
|
onTrialEnd: async ({ subscription }) => {
|
|
await triggerWorkflow(db, "trial-end", subscription);
|
|
},
|
|
onTrialExpired: async (subscription) => {
|
|
await triggerWorkflow(db, "trial-expired", subscription);
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "pro",
|
|
priceId: process.env.STRIPE_PRO_PLAN_MONTHLY_PRICE_ID!,
|
|
annualDiscountPriceId:
|
|
process.env.STRIPE_PRO_PLAN_YEARLY_PRICE_ID!,
|
|
freeTrial: {
|
|
days: 14,
|
|
onTrialStart: async (subscription) => {
|
|
await triggerWorkflow(db, "trial-start", subscription);
|
|
},
|
|
onTrialEnd: async ({ subscription }) => {
|
|
await triggerWorkflow(db, "trial-end", subscription);
|
|
},
|
|
onTrialExpired: async (subscription) => {
|
|
await triggerWorkflow(db, "trial-expired", subscription);
|
|
},
|
|
},
|
|
},
|
|
],
|
|
authorizeReference: async (data) => {
|
|
const workspace = await workspaceRepo.getByPublicId(
|
|
db,
|
|
data.referenceId,
|
|
);
|
|
|
|
if (!workspace) {
|
|
return Promise.resolve(false);
|
|
}
|
|
|
|
const isUserInWorkspace = await workspaceRepo.isUserInWorkspace(
|
|
db,
|
|
data.user.id,
|
|
workspace.id,
|
|
);
|
|
|
|
return isUserInWorkspace;
|
|
},
|
|
getCheckoutSessionParams: () => {
|
|
return {
|
|
params: {
|
|
allow_promotion_codes: true,
|
|
},
|
|
};
|
|
},
|
|
onSubscriptionComplete: async ({
|
|
subscription,
|
|
stripeSubscription,
|
|
}) => {
|
|
// Set unlimited seats to true for pro plans
|
|
if (subscription.plan === "pro") {
|
|
await subscriptionRepo.updateByStripeSubscriptionId(
|
|
db,
|
|
stripeSubscription.id,
|
|
{
|
|
unlimitedSeats: true,
|
|
},
|
|
);
|
|
console.log(
|
|
`Pro subscription ${stripeSubscription.id} activated with unlimited seats`,
|
|
);
|
|
|
|
const workspace = await workspaceRepo.getByPublicId(
|
|
db,
|
|
subscription.referenceId,
|
|
);
|
|
|
|
if (workspace?.id) {
|
|
await memberRepo.unpauseAllMembers(db, workspace.id);
|
|
}
|
|
}
|
|
},
|
|
onSubscriptionCancel: async ({
|
|
subscription,
|
|
cancellationDetails,
|
|
}) => {
|
|
await triggerWorkflow(
|
|
db,
|
|
"subscription-canceled",
|
|
subscription,
|
|
cancellationDetails,
|
|
);
|
|
|
|
// for cancelled subscriptions, we need to pause all members and set their workspace plan to free
|
|
const workspace = await workspaceRepo.getByPublicId(
|
|
db,
|
|
subscription.referenceId,
|
|
);
|
|
|
|
if (workspace?.id) {
|
|
await memberRepo.pauseAllMembers(db, workspace.id);
|
|
await workspaceRepo.update(db, subscription.referenceId, {
|
|
plan: "free",
|
|
});
|
|
}
|
|
},
|
|
onSubscriptionUpdate: async ({ subscription }) => {
|
|
await triggerWorkflow(db, "subscription-updated", subscription);
|
|
},
|
|
},
|
|
}),
|
|
]
|
|
: []),
|
|
apiKey({
|
|
enableSessionForAPIKeys: true,
|
|
rateLimit: {
|
|
enabled: true,
|
|
timeWindow: 1000 * 60, // 1 minute
|
|
maxRequests: 100, // 100 requests per minute
|
|
},
|
|
}),
|
|
magicLink({
|
|
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
|
sendMagicLink: async ({ email, url }) => {
|
|
if (url.includes("type=invite")) {
|
|
await sendEmail(
|
|
email,
|
|
"Invitation to join workspace",
|
|
"JOIN_WORKSPACE",
|
|
{
|
|
magicLoginUrl: url,
|
|
},
|
|
);
|
|
} else {
|
|
await sendEmail(email, "Sign in to kan.bn", "MAGIC_LINK", {
|
|
magicLoginUrl: url,
|
|
});
|
|
}
|
|
},
|
|
}),
|
|
// Generic OIDC provider
|
|
...(process.env.OIDC_CLIENT_ID &&
|
|
process.env.OIDC_CLIENT_SECRET &&
|
|
process.env.OIDC_DISCOVERY_URL
|
|
? [
|
|
genericOAuth({
|
|
config: [
|
|
{
|
|
providerId: "oidc",
|
|
clientId: process.env.OIDC_CLIENT_ID,
|
|
clientSecret: process.env.OIDC_CLIENT_SECRET,
|
|
discoveryUrl: process.env.OIDC_DISCOVERY_URL,
|
|
scopes: ["openid", "email", "profile"],
|
|
pkce: true,
|
|
mapProfileToUser: (profile: {
|
|
name?: string;
|
|
display_name?: string;
|
|
preferred_username?: string;
|
|
given_name?: string;
|
|
family_name?: string;
|
|
email?: string;
|
|
email_verified?: boolean;
|
|
sub?: string;
|
|
picture?: string;
|
|
avatar?: string;
|
|
}) => {
|
|
console.log("OIDC profile:", profile);
|
|
|
|
const name =
|
|
profile.name ??
|
|
profile.display_name ??
|
|
profile.preferred_username ??
|
|
(profile.given_name && profile.family_name
|
|
? `${profile.given_name} ${profile.family_name}`.trim()
|
|
: (profile.given_name ?? profile.family_name)) ??
|
|
profile.sub ??
|
|
"";
|
|
|
|
return {
|
|
email: profile.email,
|
|
name: name,
|
|
emailVerified: profile.email_verified ?? false,
|
|
image: profile.picture ?? profile.avatar ?? null,
|
|
};
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
]
|
|
: []),
|
|
];
|
|
}
|