@tribute-tg/better-auth
Version:
Tribute integration for better-auth
293 lines (286 loc) • 9.21 kB
JavaScript
import { mergeSchema } from "better-auth/db";
import { z } from "zod";
import { APIError, createAuthEndpoint, sessionMiddleware } from "better-auth/api";
import { createAuthEndpoint as createAuthEndpoint$1 } from "better-auth/plugins";
//#region src/schema.ts
const subscriptions = { subscription: { fields: {
tributeSubscriptionId: {
type: "number",
required: true
},
tributeSubscriptionName: {
type: "string",
required: true
},
tributeUserId: {
type: "number",
required: true
},
telegramUserId: {
type: "number",
required: true
},
channelId: {
type: "number",
required: true
},
period: {
type: "string",
required: true
},
price: {
type: "number",
required: true
},
amount: {
type: "number",
required: true
},
currency: {
type: "string",
required: true
},
expiresAt: {
type: "string",
required: true
},
status: {
type: "string",
defaultValue: "active"
}
} } };
const user = { user: { fields: { tributeUserId: {
type: "number",
required: false
} } } };
const getSchema = (options) => {
return mergeSchema({
...subscriptions,
...user
}, options.schema);
};
//#endregion
//#region src/util.ts
const verifyHmac = async (key, data, expectedHmac) => {
const encoder = new TextEncoder();
const keyData = encoder.encode(key);
const dataToVerify = encoder.encode(data);
const cryptoKey = await crypto.subtle.importKey("raw", keyData, {
name: "HMAC",
hash: "SHA-256"
}, false, ["sign"]);
const signature = await crypto.subtle.sign("HMAC", cryptoKey, dataToVerify);
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("") === expectedHmac;
};
//#endregion
//#region src/webhooks.ts
const webhooks = (webhooksOptions) => (tribute$1) => {
return { tributeWebhooks: createAuthEndpoint("/tribute/webhooks", {
method: "POST",
body: z.object({
name: z.string(),
created_at: z.string(),
sent_at: z.string(),
payload: z.any()
})
}, async (ctx) => {
const signature = ctx.headers?.get("Trbt-Signature");
if (!signature) return ctx.error(401);
if (!await verifyHmac(tribute$1.apiKey, JSON.stringify(ctx.body), signature)) return ctx.error(401);
const { onNewSubscription, onCancelledSubscription, onEvent } = webhooksOptions;
const event = ctx.body;
const eventName = event.name;
const payload = event.payload;
if (eventName === "new_subscription") {
await onNewSubscription?.(event.payload);
const telegramUserId = payload.telegram_user_id;
const account = await ctx.context.adapter.findOne({
model: "account",
where: [{
field: "accountId",
value: String(telegramUserId)
}, {
field: "providerId",
value: "telegram"
}]
});
if (account) {
const userId = account.userId;
const tributeUserId = payload.user_id;
await ctx.context.adapter.update({
model: "user",
update: { tributeUserId },
where: [{
field: "id",
value: userId
}]
});
await ctx.context.adapter.create({
model: "subscription",
data: {
userId,
telegramUserId,
tributeUserId: payload.user_id,
tributeSubscriptionId: payload.subscription_id,
tributeSubscriptionName: payload.subscription_name,
channelId: payload.channel_id,
period: payload.period,
price: payload.price / 100,
amount: payload.amount / 100,
currency: payload.currency,
expiresAt: payload.expires_at,
status: "active"
}
});
} else console.log("Account not found");
} else if (eventName === "cancelled_subscription") {
await onCancelledSubscription?.(payload);
await ctx.context.adapter.update({
model: "subscription",
update: { status: "cancelled" },
where: [{
field: "tributeSubscriptionId",
value: payload.subscription_id
}, {
field: "tributeUserId",
value: payload.user_id
}]
});
}
await onEvent?.(event);
}) };
};
//#endregion
//#region src/subscription.ts
const cachedSubscriptions = [];
const subscription = (options) => (tribute$1) => {
return {
upgradeSubscription: createAuthEndpoint$1("/subscription/upgrade", {
method: "POST",
body: z.object({
subscriptionId: z.number().optional(),
period: z.string().optional(),
currency: z.string().optional(),
slug: z.string().optional()
})
}, async (ctx) => {
if (!ctx.body.slug && !ctx.body.subscriptionId) throw new APIError("BAD_REQUEST", { message: "Either slug or subscriptionId is required" });
const findFrom = (subscriptions$1) => subscriptions$1.find((subscription$2) => (!ctx.body.subscriptionId || subscription$2.subscriptionId === ctx.body.subscriptionId) && (!ctx.body.slug || subscription$2.slug === ctx.body.slug) && (!ctx.body.period || subscription$2.period === ctx.body.period) && (!ctx.body.currency || subscription$2.currency === ctx.body.currency));
let subscription$1 = findFrom(cachedSubscriptions);
if (!subscription$1) {
const resolvedSubscription = findFrom(await (typeof options.subscriptions === "function" ? options.subscriptions() : options.subscriptions) ?? []);
if (resolvedSubscription) subscription$1 = resolvedSubscription;
}
if (!subscription$1) throw new APIError("BAD_REQUEST", { message: "Subscription not found" });
try {
const cachedLink = subscription$1.redirectUrl?.trim();
if (cachedLink) return ctx.json({
url: cachedLink,
redirect: true
});
ctx.context.logger.info(`Fetching subscription link for ${subscription$1.subscriptionId}`);
const url = (await tribute$1.getSubscription(subscription$1.subscriptionId)).subscription.webLink;
const index = cachedSubscriptions.findIndex((s) => s.subscriptionId === subscription$1.subscriptionId);
if (index === -1) cachedSubscriptions.push({
...subscription$1,
redirectUrl: url
});
else cachedSubscriptions[index] = {
...subscription$1,
redirectUrl: url
};
return ctx.json({
url,
redirect: true
});
} catch (e) {
if (e instanceof Error) ctx.context.logger.error(`Tribute checkout creation failed. Error: ${e.message}`);
throw new APIError("INTERNAL_SERVER_ERROR", { message: "Checkout creation failed" });
}
}),
listActiveSubscriptions: createAuthEndpoint$1("/subscription/list", {
method: "GET",
use: [sessionMiddleware]
}, async (ctx) => {
if (!ctx.context.session.user.id || !ctx.context.session.user["tributeUserId"]) throw new APIError("BAD_REQUEST", { message: "User not found" });
try {
const activeSubscriptions = (await ctx.context.adapter.findMany({
model: "subscription",
where: [{
field: "tributeUserId",
value: ctx.context.session.user["tributeUserId"]
}]
})).filter((s) => {
return new Date(s.expiresAt) > /* @__PURE__ */ new Date() || s.status === "active";
});
return ctx.json(activeSubscriptions);
} catch (e) {
if (e instanceof Error) ctx.context.logger.error(`Tribute subscriptions list failed. Error: ${e.message}`);
throw new APIError("INTERNAL_SERVER_ERROR", { message: "Subscriptions list failed" });
}
}),
portal: createAuthEndpoint$1("/customer/portal", {
method: "GET",
use: [sessionMiddleware]
}, async (ctx) => {
if (!ctx.context.session?.user.id) throw new APIError("BAD_REQUEST", { message: "User not found" });
try {
return ctx.json({
url: "https://t.me/tribute/app?startapp=",
redirect: true
});
} catch (e) {
if (e instanceof Error) ctx.context.logger.error(`Tribute customer portal creation failed. Error: ${e.message}`);
throw new APIError("INTERNAL_SERVER_ERROR", { message: "Customer portal creation failed" });
}
}),
state: createAuthEndpoint$1("/customer/state", {
method: "GET",
use: [sessionMiddleware]
}, async (ctx) => {
if (!ctx.context.session.user.id || !ctx.context.session.user["tributeUserId"]) throw new APIError("BAD_REQUEST", { message: "User not found" });
try {
const activeSubscriptions = (await ctx.context.adapter.findMany({
model: "subscription",
where: [{
field: "tributeUserId",
value: ctx.context.session.user["tributeUserId"]
}]
})).filter((s) => {
return new Date(s.expiresAt) > /* @__PURE__ */ new Date() || s.status === "active";
});
const state = {
...ctx.context.session.user,
activeSubscriptions
};
return ctx.json(state);
} catch (e) {
if (e instanceof Error) ctx.context.logger.error(`Tribute customer state failed. Error: ${e.message}`);
throw new APIError("INTERNAL_SERVER_ERROR", { message: "Customer state failed" });
}
})
};
};
//#endregion
//#region src/client.ts
const tributeClient = () => {
return {
id: "tribute-client",
$InferServerPlugin: {}
};
};
//#endregion
//#region src/index.ts
const tribute = (options) => {
return {
id: "tribute",
endpoints: { ...[webhooks(options), subscription(options)].map((use) => use(options.tributeClient)).reduce((acc, plugin) => {
Object.assign(acc, plugin);
return acc;
}, {}) },
schema: getSchema(options)
};
};
//#endregion
export { subscription, tribute, tributeClient, webhooks };
//# sourceMappingURL=index.mjs.map