UNPKG

@convex-dev/better-auth

Version:
247 lines 9.72 kB
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