UNPKG

@ley0x/better-auth-lastfm

Version:

Last.fm authentication plugin for BetterAuth

192 lines (187 loc) 7.2 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { createLastfmApiSignature: () => createLastfmApiSignature, lastfmPlugin: () => lastfmPlugin }); module.exports = __toCommonJS(index_exports); // src/server.ts var import_api = require("better-auth/api"); var import_zod = require("zod"); // src/utils/api-signature.ts var import_crypto = require("crypto"); function createLastfmApiSignature(params, secret) { const sortedParams = Object.keys(params).sort().map((key) => `${key}${params[key]}`).join(""); return (0, import_crypto.createHash)("md5").update(sortedParams + secret).digest("hex"); } // src/server.ts var lastfmResponseSchema = import_zod.z.object({ session: import_zod.z.object({ key: import_zod.z.string(), name: import_zod.z.string(), subscriber: import_zod.z.number() }) }); var userSchema = import_zod.z.object({ id: import_zod.z.string(), name: import_zod.z.string(), email: import_zod.z.string(), emailVerified: import_zod.z.boolean(), image: import_zod.z.string().nullish(), createdAt: import_zod.z.date(), updatedAt: import_zod.z.date() }); var accountSchema = import_zod.z.object({ id: import_zod.z.string(), accountId: import_zod.z.string(), providerId: import_zod.z.string(), userId: import_zod.z.string(), accessToken: import_zod.z.string().nullish(), refreshToken: import_zod.z.string().nullish(), idToken: import_zod.z.string().nullish(), accessTokenExpiresAt: import_zod.z.date().nullish(), refreshTokenExpiresAt: import_zod.z.date().nullish(), scope: import_zod.z.string().nullish(), password: import_zod.z.string().nullish(), createdAt: import_zod.z.date(), updatedAt: import_zod.z.date() }); function lastfmPlugin(options) { const { apiKey, sharedSecret, baseUrl = process.env.BETTER_AUTH_URL || "http://localhost:3000", redirectTo = "/dashboard" } = options; if (!apiKey || !sharedSecret) { throw new Error("Last.fm plugin requires both apiKey and sharedSecret"); } return { id: "lastfm", endpoints: { "/lastfm/signin": (0, import_api.createAuthEndpoint)("/lastfm/signin", { method: "GET" }, async (ctx) => { const callbackUrl = `${baseUrl}/api/auth/lastfm/callback`; const authUrl = `https://www.last.fm/api/auth/?api_key=${apiKey}&cb=${encodeURIComponent(callbackUrl)}`; return ctx.redirect(authUrl); }), "/lastfm/callback": (0, import_api.createAuthEndpoint)("/lastfm/callback", { method: "GET" }, async (ctx) => { const { token } = ctx.query; if (!token) { ctx.context.logger?.error("Last.fm callback: Missing token parameter"); return ctx.json({ error: "Authentication failed: Missing token" }, { status: 400 }); } try { const sessionData = await exchangeTokenForSession(token, apiKey, sharedSecret); const { username, sessionKey } = sessionData; const existingAccount = await ctx.context.adapter.findOne({ model: "account", where: [ { field: "providerId", value: "lastfm" }, { field: "accountId", value: username } ] }); let user; if (existingAccount) { const validatedAccount = accountSchema.parse(existingAccount); await ctx.context.adapter.update({ model: "account", where: [{ field: "id", value: validatedAccount.id }], update: { accessToken: sessionKey, updatedAt: /* @__PURE__ */ new Date() } }); const existingUser = await ctx.context.adapter.findOne({ model: "user", where: [{ field: "id", value: validatedAccount.userId }] }); if (!existingUser) { return ctx.json({ error: "User not found" }, { status: 404 }); } user = userSchema.parse(existingUser); } else { const newUser = await ctx.context.adapter.create({ model: "user", data: { name: username, email: `${username}@lastfm.local`, emailVerified: true, image: null } }); user = userSchema.parse(newUser); await ctx.context.adapter.create({ model: "account", data: { accountId: username, providerId: "lastfm", userId: user.id, accessToken: sessionKey, createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() } }); } const session = await ctx.context.internalAdapter.createSession(user.id); const cookieName = ctx.context.authCookies.sessionToken.name; const cookieOptions = ctx.context.authCookies.sessionToken.attributes; await ctx.setSignedCookie(cookieName, session.token, ctx.context.secret, { ...cookieOptions, maxAge: cookieOptions.maxAge || void 0 }); return ctx.redirect(redirectTo); } catch (error) { ctx.context.logger?.error("Last.fm authentication error:", error); return ctx.json({ error: "Authentication failed" }, { status: 500 }); } }) } }; } async function exchangeTokenForSession(token, apiKey, sharedSecret) { const params = { api_key: apiKey, method: "auth.getSession", token }; const apiSignature = createLastfmApiSignature(params, sharedSecret); const url = new URL("https://ws.audioscrobbler.com/2.0/"); url.searchParams.set("method", "auth.getSession"); url.searchParams.set("api_key", apiKey); url.searchParams.set("token", token); url.searchParams.set("api_sig", apiSignature); url.searchParams.set("format", "json"); const response = await fetch(url.toString()); if (!response.ok) { throw new Error(`Last.fm API error: ${response.status} ${response.statusText}`); } const data = await response.json(); const validatedData = lastfmResponseSchema.parse(data); return { username: validatedData.session.name, sessionKey: validatedData.session.key }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { createLastfmApiSignature, lastfmPlugin });