Skip to content

Instantly share code, notes, and snippets.

@bkyerv
Created April 9, 2025 08:35
Show Gist options
  • Save bkyerv/a73ea2c49cc89c6261e19ff07004dd6d to your computer and use it in GitHub Desktop.
Save bkyerv/a73ea2c49cc89c6261e19ff07004dd6d to your computer and use it in GitHub Desktop.
telegram plugin
import crypto from "node:crypto";
import { createAuthEndpoint } from "better-auth/api";
import { account, user } from "src/db/auth-schema";
import type { BetterAuthPlugin } from "better-auth/types";
// const TELEGRAM_BOT_TOKEN = import.meta.env.TELEGRAM_BOT_TOKEN;
// The session expiration time (30 days by default)
const SESSION_MAX_AGE = 60 * 60 * 24 * 30; // 30 days in seconds
// Helper to generate placeholder emails
const createPlaceholderEmail = (telegramUserId: string): string => {
return `tg_${telegramUserId}@placeholder.telegram.user`;
};
export const telegramPlugin = ({ botToken }: { botToken: string }) => {
return {
id: "telegram",
endpoints: {
telegramCallback: createAuthEndpoint(
"/telegram/callback",
{ method: "GET", requireRequest: true },
async ({
request,
context: { adapter, internalAdapter, createAuthCookie },
redirect,
setCookie,
setSignedCookie,
}) => {
console.log("from inside telegram callback - bot token", botToken);
if (!botToken) {
console.error("Telegram Bot Token not configured!");
return new Response("Server configuration error", { status: 500 });
}
const url = new URL(request?.url);
const params = url.searchParams;
const hash = params.get("hash");
const telegramUserData = Object.fromEntries(params.entries());
if (!hash) {
return redirect("/signin?error=telegram_missing_hash");
}
// --- 1. Verify Telegram Hash ---
const dataCheckArr: string[] = [];
Object.keys(telegramUserData)
.filter((key) => key !== "hash")
.sort()
.forEach((key) => {
dataCheckArr.push(`${key}=${telegramUserData[key]}`);
});
const dataCheckString = dataCheckArr.join("\n");
console.log(
"from inside telegram callback - datacheckstring",
dataCheckString
);
const secretKey = crypto
.createHash("sha256")
.update(botToken)
.digest();
const calculatedHash = crypto
.createHmac("sha256", secretKey)
.update(dataCheckString)
.digest("hex");
if (calculatedHash !== hash) {
console.warn("Telegram hash verification failed!");
return redirect("/signin?error=telegram_auth_failed");
}
try {
// --- Check auth_date ---
const authDate = params.get("auth_date");
if (authDate) {
const timestamp = parseInt(authDate, 10);
const now = Math.floor(Date.now() / 1000);
const timeDiff = now - timestamp;
if (timeDiff > 300) {
// 5 minute validity window
console.warn("Telegram auth_date is too old!");
return redirect("/signin?error=telegram_auth_expired");
}
} else {
return redirect("/signin?error=telegram_auth_timestamp_missing");
}
// --- Hash is valid ---
const telegramUserId = params.get("id"); // This is the providerAccountId
if (!telegramUserId) {
return redirect("/signin?error=telegram_id_missing");
}
const providerId = "telegram";
// Step 1: Check for an existing Telegram account
const existingAccount = (await adapter.findOne({
model: "account",
where: [
{ field: "providerId", value: providerId },
{ field: "accountId", value: telegramUserId },
],
select: ["userId"],
})) as typeof account.$inferSelect;
let userId: string | null = null;
let userJustCreated = false;
if (existingAccount) {
// Step 2a: Account exists, use the linked user
userId = existingAccount.userId;
console.log(
`Found existing user yo yo yo (${userId}) via Telegram account.`
);
} else {
// Step 2b: No account found, create a new user and link the account
console.log(
`No existing Telegram account found for ${telegramUserId}. Creating new user.`
);
userJustCreated = true;
// Construct user data
const userName =
`${params.get("first_name") || ""} ${
params.get("last_name") || ""
}`.trim() ||
params.get("username") ||
`Telegram User ${telegramUserId}`;
// Create the user
const newUser = (await adapter.create({
model: "user",
data: {
name: userName,
email: createPlaceholderEmail(telegramUserId),
emailVerified: false,
image: params.get("photo_url") || null,
role: "regular_user",
},
})) as typeof user.$inferInsert;
userId = newUser.id;
console.log(`Created new user with ID: ${userId}`);
// Link the Telegram account to the new user
await adapter.create({
model: "account",
data: {
userId: userId,
providerId: providerId,
accountId: telegramUserId,
},
});
console.log(
`Linked Telegram account ${telegramUserId} to user ${userId}`
);
}
const session = await internalAdapter.createSession(
userId,
request
);
const cookie = createAuthCookie("session_token", {
maxAge: SESSION_MAX_AGE,
});
console.log("Cookie attributes:", cookie.attributes);
// setSignedCookie(
// cookie.name,
// session.token,
// process.env.BETTER_AUTH_SECRET!,
// cookie.attributes
// );
setCookie(cookie.name, session.token, cookie.attributes);
console.log("Signed cookie set with name:", cookie.name);
console.log("Session token:", session.token);
// Redirect based on user status
const redirectTo = userJustCreated
? "/protected/new"
: "/protected/images/results";
return redirect(redirectTo);
} catch (error) {
console.error("Error during Telegram auth callback:", error);
if (error instanceof Error) {
console.error(error.message);
console.error(error.stack);
}
return redirect("/signin?error=server_error");
}
}
),
},
} satisfies BetterAuthPlugin;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment