UNPKG

@convex-dev/better-auth

Version:
311 lines 12.1 kB
import { httpActionGeneric, internalMutationGeneric, queryGeneric, } from "convex/server"; import { ConvexError, v } from "convex/values"; import { convexAdapter } from "./adapter.js"; import { corsRouter } from "convex-helpers/server/cors"; /** * Backend API for the Better Auth component. * Responsible for exposing the `client` and `triggers` APIs to the client, http * route registration, and having convenience methods for interacting with the * component from the backend. * * @param component - Generally `components.betterAuth` from * `./_generated/api` once you've configured it in `convex.config.ts`. * @param config - Configuration options for the component. * @param config.local - Local schema configuration. * @param config.verbose - Whether to enable verbose logging. * @param config.triggers - Triggers configuration. * @param config.authFunctions - Authentication functions configuration. */ export const createClient = (component, config) => { const safeGetAuthUser = async (ctx) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { return; } const session = (await ctx.runQuery(component.adapter.findOne, { model: "session", where: [ { field: "_id", value: identity.sessionId, }, { field: "expiresAt", operator: "gt", value: new Date().getTime(), }, ], })); if (!session) { return; } const doc = (await ctx.runQuery(component.adapter.findOne, { model: "user", where: [ { field: "_id", value: identity.subject, }, ], })); if (!doc) { return; } return doc; }; const getAuthUser = async (ctx) => { const user = await safeGetAuthUser(ctx); if (!user) { throw new ConvexError("Unauthenticated"); } return user; }; const getHeaders = async (ctx) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { return new Headers(); } // Don't validate the session here, let Better Auth handle that const session = await ctx.runQuery(component.adapter.findOne, { model: "session", where: [ { field: "_id", value: identity.sessionId, }, ], }); return new Headers({ ...(session?.token ? { authorization: `Bearer ${session.token}` } : {}), ...(session?.ipAddress ? { "x-forwarded-for": session.ipAddress } : {}), }); }; return { /** * Returns the Convex database adapter for use in Better Auth options. * @param ctx - The Convex context * @returns The Convex database adapter */ adapter: (ctx) => convexAdapter(ctx, component, { ...config, debugLogs: config?.verbose, }), /** * Returns the Better Auth auth object and headers for using Better Auth API * methods directly in a Convex mutation or query. Convex functions don't * have access to request headers, so the headers object is created at * runtime with the token for the current session as a Bearer token. * * @param createAuth - The createAuth function * @param ctx - The Convex context * @returns A promise that resolves to the Better Auth `auth` API object and * headers. */ getAuth: async (createAuth, ctx) => ({ auth: createAuth(ctx), headers: await getHeaders(ctx), }), /** * Returns a Headers object for the current session using the session id * from the current user identity via `ctx.auth.getUserIdentity()`. This is * used to pass the headers to the Better Auth API methods when using the * `getAuth` method. * * @param ctx - The Convex context * @returns The headers */ getHeaders, /** * Returns the current user or null if the user is not found * @param ctx - The Convex context * @returns The user or null if the user is not found */ safeGetAuthUser, /** * Returns the current user or throws an error if the user is not found * * @param ctx - The Convex context * @returns The user or throws an error if the user is not found */ getAuthUser, /** * Returns a user by their Better Auth user id. * @param ctx - The Convex context * @param id - The Better Auth user id * @returns The user or null if the user is not found */ getAnyUserById: async (ctx, id) => { return (await ctx.runQuery(component.adapter.findOne, { model: "user", where: [{ field: "_id", value: id }], })); }, /** * Replaces 0.7 behavior of returning a new user id from * onCreateUser * @param ctx - The Convex context * @param authId - The Better Auth user id * @param userId - The app user id * @deprecated in 0.9 */ setUserId: async (ctx, authId, userId) => { await ctx.runMutation(component.adapter.updateOne, { input: { model: "user", where: [{ field: "_id", value: authId }], update: { userId }, }, }); }, /** * Exposes functions for use with the ClientAuthBoundary component. Currently * only contains getAuthUser. * @returns Functions to pass to the ClientAuthBoundary component. */ clientApi: () => ({ /** * Convex query to get the current user. For use with the ClientAuthBoundary component. * * ```ts title="convex/auth.ts" * export const { getAuthUser } = authComponent.clientApi(); * ``` * * @returns The user or throws an error if the user is not found */ getAuthUser: queryGeneric({ args: {}, handler: async (ctx) => { return await getAuthUser(ctx); }, }), }), /** * Exposes functions for executing trigger callbacks in the app context. * * Callbacks are defined in the `triggers` option to the component client config. * * See {@link createClient} for more information. * * @returns Functions to execute trigger callbacks in the app context. */ triggersApi: () => ({ onCreate: internalMutationGeneric({ args: { doc: v.any(), model: v.string(), }, handler: async (ctx, args) => { await config?.triggers?.[args.model]?.onCreate?.(ctx, args.doc); }, }), onUpdate: internalMutationGeneric({ args: { oldDoc: v.any(), newDoc: v.any(), model: v.string(), }, handler: async (ctx, args) => { await config?.triggers?.[args.model]?.onUpdate?.(ctx, args.newDoc, args.oldDoc); }, }), onDelete: internalMutationGeneric({ args: { doc: v.any(), model: v.string(), }, handler: async (ctx, args) => { await config?.triggers?.[args.model]?.onDelete?.(ctx, args.doc); }, }), }), registerRoutes: (http, createAuth, opts = {}) => { const staticAuth = createAuth({}); const path = staticAuth.options.basePath ?? "/api/auth"; const authRequestHandler = httpActionGeneric(async (ctx, request) => { if (config?.verbose) { // eslint-disable-next-line no-console console.log("options.baseURL", staticAuth.options.baseURL); // eslint-disable-next-line no-console console.log("request headers", request.headers); } const auth = createAuth(ctx); const response = await auth.handler(request); if (config?.verbose) { // eslint-disable-next-line no-console console.log("response headers", response.headers); } return response; }); const wellKnown = http.lookup("/.well-known/openid-configuration", "GET"); // If registerRoutes is used multiple times, this may already be defined if (!wellKnown) { // Redirect root well-known to api well-known http.route({ path: "/.well-known/openid-configuration", method: "GET", handler: httpActionGeneric(async () => { const url = `${process.env.CONVEX_SITE_URL}${path}/convex/.well-known/openid-configuration`; return Response.redirect(url); }), }); } if (!opts.cors) { http.route({ pathPrefix: `${path}/`, method: "GET", handler: authRequestHandler, }); http.route({ pathPrefix: `${path}/`, method: "POST", handler: authRequestHandler, }); return; } const corsOpts = typeof opts.cors === "boolean" ? { allowedOrigins: [], allowedHeaders: [], exposedHeaders: [] } : opts.cors; let trustedOriginsOption; const cors = corsRouter(http, { allowedOrigins: async (request) => { trustedOriginsOption = trustedOriginsOption ?? (await staticAuth.$context).options.trustedOrigins ?? []; const trustedOrigins = Array.isArray(trustedOriginsOption) ? trustedOriginsOption : await trustedOriginsOption(request); return trustedOrigins .map((origin) => // Strip trailing wildcards, unsupported for allowedOrigins origin.endsWith("*") && origin.length > 1 ? origin.slice(0, -1) : origin) .concat(corsOpts.allowedOrigins ?? []); }, allowCredentials: true, allowedHeaders: [ "Content-Type", "Better-Auth-Cookie", "Authorization", ].concat(corsOpts.allowedHeaders ?? []), exposedHeaders: ["Set-Better-Auth-Cookie"].concat(corsOpts.exposedHeaders ?? []), debug: config?.verbose, enforceAllowOrigins: false, }); cors.route({ pathPrefix: `${path}/`, method: "GET", handler: authRequestHandler, }); cors.route({ pathPrefix: `${path}/`, method: "POST", handler: authRequestHandler, }); }, }; }; //# sourceMappingURL=create-client.js.map