@convex-dev/better-auth
Version:
A Better Auth component for Convex.
311 lines • 12.1 kB
JavaScript
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