@convex-dev/better-auth
Version:
A Better Auth component for Convex.
247 lines • 9.72 kB
JavaScript
import { httpActionGeneric, internalMutationGeneric, queryGeneric, } from "convex/server";
import { v } from "convex/values";
import schema from "../component/schema";
import { convexAdapter } from "./adapter";
import { omit } from "convex-helpers";
import { createCookieGetter } from "better-auth/cookies";
import { fetchQuery } from "convex/nextjs";
import { JWT_COOKIE_NAME } from "../plugins/convex";
import { requireEnv } from "../utils";
import { partial } from "convex-helpers/validators";
import { adapterArgsValidator, adapterWhereValidator } from "../component/lib";
import { corsRouter } from "convex-helpers/server/cors";
import { version as convexVersion } from "convex";
import semver from "semver";
export { convexAdapter };
if (semver.lt(convexVersion, "1.25.0")) {
throw new Error("Convex version must be at least 1.25.0");
}
const createUserFields = omit(schema.tables.user.validator.fields, ["userId"]);
const createUserValidator = v.object(createUserFields);
const createUserArgsValidator = v.object({
input: v.object({
model: v.literal("user"),
data: v.object(createUserFields),
}),
});
const updateUserArgsValidator = v.object({
input: v.object({
model: v.literal("user"),
where: v.optional(v.array(adapterWhereValidator)),
update: v.object(partial(createUserFields)),
}),
});
const createSessionArgsValidator = v.object({
input: v.object({
model: v.literal("session"),
data: v.object(schema.tables.session.validator.fields),
}),
});
export class BetterAuth {
component;
config;
constructor(component, config) {
this.component = component;
this.config = config;
}
async isAuthenticated(token) {
if (!this.config.publicAuthFunctions?.isAuthenticated) {
throw new Error("isAuthenticated function not found. It must be a named export in convex/auth.ts");
}
return fetchQuery(this.config.publicAuthFunctions.isAuthenticated, {}, { token: token ?? undefined });
}
async getHeaders(ctx) {
const session = await ctx.runQuery(this.component.lib.getCurrentSession);
return new Headers({
...(session?.token ? { authorization: `Bearer ${session.token}` } : {}),
...(session?.ipAddress ? { "x-forwarded-for": session.ipAddress } : {}),
});
}
// TODO: use the proper id type for auth functions
async getAuthUserId(ctx) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return null;
}
return identity.subject;
}
// Convenience function for getting the Better Auth user
async getAuthUser(ctx) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return null;
}
const doc = await ctx.runQuery(this.component.lib.findOne, {
model: "user",
where: [
{
field: "userId",
value: identity.subject,
},
],
});
if (!doc) {
return null;
}
// Type narrowing
if (!("emailVerified" in doc)) {
throw new Error("invalid user");
}
const { id: _id, ...user } = doc;
return user;
}
async getIdTokenCookieName(createAuth) {
const auth = createAuth({});
const createCookie = createCookieGetter(auth.options);
const cookie = createCookie(JWT_COOKIE_NAME);
return cookie.name;
}
async updateUserMetadata(ctx, userId, metadata) {
return ctx.runMutation(this.component.lib.updateOne, {
input: {
model: "user",
where: [{ field: "userId", value: userId }],
update: metadata,
},
});
}
async getUserByUsername(ctx, username) {
return ctx.runQuery(this.component.lib.findOne, {
model: "user",
where: [{ field: "username", value: username }],
});
}
createAuthFunctions(opts) {
return {
isAuthenticated: queryGeneric({
args: v.object({}),
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
return identity !== null;
},
}),
createUser: internalMutationGeneric({
args: createUserArgsValidator,
handler: async (ctx, args) => {
const userId = await opts.onCreateUser(ctx, args.input.data);
return ctx.runMutation(this.component.lib.create, {
input: {
...args.input,
data: { ...args.input.data, userId },
},
});
},
}),
deleteUser: internalMutationGeneric({
args: adapterArgsValidator,
handler: async (ctx, args) => {
const doc = await ctx.runMutation(this.component.lib.deleteOne, args);
if (doc && opts.onDeleteUser) {
await opts.onDeleteUser(ctx, doc.userId);
}
return doc;
},
}),
updateUser: internalMutationGeneric({
args: updateUserArgsValidator,
handler: async (ctx, args) => {
const updatedUser = await ctx.runMutation(this.component.lib.updateOne, { input: args.input });
// Type narrowing
if (!("emailVerified" in updatedUser)) {
throw new Error("invalid user");
}
if (opts.onUpdateUser) {
await opts.onUpdateUser(ctx, omit(updatedUser, ["_id"]));
}
return updatedUser;
},
}),
createSession: internalMutationGeneric({
args: createSessionArgsValidator,
handler: async (ctx, args) => {
const session = await ctx.runMutation(this.component.lib.create, {
input: args.input,
});
await opts.onCreateSession?.(ctx, session);
return session;
},
}),
};
}
registerRoutes(http, createAuth, opts = {}) {
const betterAuthOptions = createAuth({}).options;
const path = betterAuthOptions.basePath ?? "/api/auth";
const authRequestHandler = httpActionGeneric(async (ctx, request) => {
if (this.config.verbose) {
console.log("options.baseURL", betterAuthOptions.baseURL);
console.log("request headers", request.headers);
}
const auth = createAuth(ctx);
const response = await auth.handler(request);
if (this.config?.verbose) {
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 = `${requireEnv("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;
const cors = corsRouter(http, {
allowedOrigins: async (request) => {
const trustedOriginsOption = (await createAuth({}).$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"].concat(corsOpts.allowedHeaders ?? []),
exposedHeaders: ["Set-Better-Auth-Cookie"].concat(corsOpts.exposedHeaders ?? []),
debug: this.config?.verbose,
enforceAllowOrigins: false,
});
cors.route({
pathPrefix: `${path}/`,
method: "GET",
handler: authRequestHandler,
});
cors.route({
pathPrefix: `${path}/`,
method: "POST",
handler: authRequestHandler,
});
}
}
//# sourceMappingURL=index.js.map