Add Better Auth admin authentication

This commit is contained in:
2026-06-04 12:05:07 +02:00
parent 0f10bd6400
commit e660ec24aa
41 changed files with 2225 additions and 284 deletions

View File

@@ -12,6 +12,7 @@ import type * as audits from "../audits.js";
import type * as blacklist from "../blacklist.js";
import type * as campaigns from "../campaigns.js";
import type * as domain from "../domain.js";
import type * as http from "../http.js";
import type * as leads from "../leads.js";
import type * as outreach from "../outreach.js";
import type * as runs from "../runs.js";
@@ -29,6 +30,7 @@ declare const fullApi: ApiFromModules<{
blacklist: typeof blacklist;
campaigns: typeof campaigns;
domain: typeof domain;
http: typeof http;
leads: typeof leads;
outreach: typeof outreach;
runs: typeof runs;
@@ -62,4 +64,6 @@ export declare const internal: FilterApi<
FunctionReference<any, "internal">
>;
export declare const components: {};
export declare const components: {
betterAuth: import("../betterAuth/_generated/component.js").ComponentApi<"betterAuth">;
};

6
convex/auth.config.ts Normal file
View File

@@ -0,0 +1,6 @@
import { getAuthConfigProvider } from "@convex-dev/better-auth/auth-config";
import type { AuthConfig } from "convex/server";
export default {
providers: [getAuthConfigProvider()],
} satisfies AuthConfig;

View File

@@ -0,0 +1,54 @@
/* eslint-disable */
/**
* Generated `api` utility.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import type * as adapter from "../adapter.js";
import type * as auth from "../auth.js";
import type * as env from "../env.js";
import type {
ApiFromModules,
FilterApi,
FunctionReference,
} from "convex/server";
import { anyApi, componentsGeneric } from "convex/server";
const fullApi: ApiFromModules<{
adapter: typeof adapter;
auth: typeof auth;
env: typeof env;
}> = anyApi as any;
/**
* A utility for referencing Convex functions in your app's public API.
*
* Usage:
* ```js
* const myFunctionReference = api.myModule.myFunction;
* ```
*/
export const api: FilterApi<
typeof fullApi,
FunctionReference<any, "public">
> = anyApi as any;
/**
* A utility for referencing Convex functions in your app's internal API.
*
* Usage:
* ```js
* const myFunctionReference = internal.myModule.myFunction;
* ```
*/
export const internal: FilterApi<
typeof fullApi,
FunctionReference<any, "internal">
> = anyApi as any;
export const components = componentsGeneric() as unknown as {};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,60 @@
/* eslint-disable */
/**
* Generated data model types.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import type {
DataModelFromSchemaDefinition,
DocumentByName,
TableNamesInDataModel,
SystemTableNames,
} from "convex/server";
import type { GenericId } from "convex/values";
import schema from "../schema.js";
/**
* The names of all of your Convex tables.
*/
export type TableNames = TableNamesInDataModel<DataModel>;
/**
* The type of a document stored in Convex.
*
* @typeParam TableName - A string literal type of the table name (like "users").
*/
export type Doc<TableName extends TableNames> = DocumentByName<
DataModel,
TableName
>;
/**
* An identifier for a document in Convex.
*
* Convex documents are uniquely identified by their `Id`, which is accessible
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
*
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
*
* IDs are just strings at runtime, but this type can be used to distinguish them from other
* strings when type checking.
*
* @typeParam TableName - A string literal type of the table name (like "users").
*/
export type Id<TableName extends TableNames | SystemTableNames> =
GenericId<TableName>;
/**
* A type describing your Convex data model.
*
* This type includes information about what tables you have, the type of
* documents stored in those tables, and the indexes defined on them.
*
* This type is used to parameterize methods like `queryGeneric` and
* `mutationGeneric` to make them type-safe.
*/
export type DataModel = DataModelFromSchemaDefinition<typeof schema>;

View File

@@ -0,0 +1,156 @@
/* eslint-disable */
/**
* Generated utilities for implementing server-side Convex query and mutation functions.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import type {
ActionBuilder,
HttpActionBuilder,
MutationBuilder,
QueryBuilder,
GenericActionCtx,
GenericMutationCtx,
GenericQueryCtx,
GenericDatabaseReader,
GenericDatabaseWriter,
} from "convex/server";
import {
actionGeneric,
httpActionGeneric,
queryGeneric,
mutationGeneric,
internalActionGeneric,
internalMutationGeneric,
internalQueryGeneric,
} from "convex/server";
import type { DataModel } from "./dataModel.js";
/**
* Define a query in this Convex app's public API.
*
* This function will be allowed to read your Convex database and will be accessible from the client.
*
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
*/
export const query: QueryBuilder<DataModel, "public"> = queryGeneric;
/**
* Define a query that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
*
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
*/
export const internalQuery: QueryBuilder<DataModel, "internal"> =
internalQueryGeneric;
/**
* Define a mutation in this Convex app's public API.
*
* This function will be allowed to modify your Convex database and will be accessible from the client.
*
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
*/
export const mutation: MutationBuilder<DataModel, "public"> = mutationGeneric;
/**
* Define a mutation that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
*
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
*/
export const internalMutation: MutationBuilder<DataModel, "internal"> =
internalMutationGeneric;
/**
* Define an action in this Convex app's public API.
*
* An action is a function which can execute any JavaScript code, including non-deterministic
* code and code with side-effects, like calling third-party services.
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
*
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
*/
export const action: ActionBuilder<DataModel, "public"> = actionGeneric;
/**
* Define an action that is only accessible from other Convex functions (but not from the client).
*
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
*/
export const internalAction: ActionBuilder<DataModel, "internal"> =
internalActionGeneric;
/**
* Define an HTTP action.
*
* The wrapped function will be used to respond to HTTP requests received
* by a Convex deployment if the requests matches the path and method where
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
*
* @param func - The function. It receives an {@link ActionCtx} as its first argument
* and a Fetch API `Request` object as its second.
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
*/
export const httpAction: HttpActionBuilder = httpActionGeneric;
/**
* A set of services for use within Convex query functions.
*
* The query context is passed as the first argument to any Convex query
* function run on the server.
*
* If you're using code generation, use the `QueryCtx` type in `convex/_generated/server.d.ts` instead.
*/
export type QueryCtx = GenericQueryCtx<DataModel>;
/**
* A set of services for use within Convex mutation functions.
*
* The mutation context is passed as the first argument to any Convex mutation
* function run on the server.
*
* If you're using code generation, use the `MutationCtx` type in `convex/_generated/server.d.ts` instead.
*/
export type MutationCtx = GenericMutationCtx<DataModel>;
/**
* A set of services for use within Convex action functions.
*
* The action context is passed as the first argument to any Convex action
* function run on the server.
*/
export type ActionCtx = GenericActionCtx<DataModel>;
/**
* An interface to read from the database within Convex query functions.
*
* The two entry points are {@link DatabaseReader.get}, which fetches a single
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
* building a query.
*/
export type DatabaseReader = GenericDatabaseReader<DataModel>;
/**
* An interface to read from and write to the database within Convex mutation
* functions.
*
* Convex guarantees that all writes within a single mutation are
* executed atomically, so you never have to worry about partial writes leaving
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
* for the guarantees Convex provides your functions.
*/
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;

View File

@@ -0,0 +1,17 @@
import { createApi } from "@convex-dev/better-auth";
import { createAuthOptions } from "./auth.js";
import schema from "./schema.js";
const createSchemaAuthOptions = (ctx: Parameters<typeof createAuthOptions>[0]) =>
createAuthOptions(ctx, { allowMissingSecret: true });
export const {
create,
findOne,
findMany,
updateOne,
updateMany,
deleteOne,
deleteMany,
} = createApi(schema, createSchemaAuthOptions);

59
convex/betterAuth/auth.ts Normal file
View File

@@ -0,0 +1,59 @@
import { createClient } from "@convex-dev/better-auth";
import { convex } from "@convex-dev/better-auth/plugins";
import type { GenericCtx } from "@convex-dev/better-auth";
import { betterAuth, type BetterAuthOptions } from "better-auth/minimal";
import type { FunctionReference } from "convex/server";
import { components } from "../_generated/api";
import type { DataModel } from "../_generated/dataModel";
import authConfig from "../auth.config";
import { getBetterAuthServerConfig } from "./env";
import schema from "./schema";
type BetterAuthComponentApi = {
adapter: {
create: FunctionReference<"mutation", "internal">;
findOne: FunctionReference<"query", "internal">;
findMany: FunctionReference<"query", "internal">;
updateOne: FunctionReference<"mutation", "internal">;
updateMany: FunctionReference<"mutation", "internal">;
deleteOne: FunctionReference<"mutation", "internal">;
deleteMany: FunctionReference<"mutation", "internal">;
};
};
const componentsWithAuth = components as {
betterAuth: BetterAuthComponentApi;
};
export const authComponent = createClient<DataModel, typeof schema>(
componentsWithAuth.betterAuth,
{
local: { schema },
verbose: false,
},
);
export const createAuthOptions = (
ctx: GenericCtx<DataModel>,
options: { allowMissingSecret?: boolean } = {},
) => {
const { appUrl, secret } = getBetterAuthServerConfig(process.env, options);
return {
appName: "WebDev Pipeline",
baseURL: appUrl,
secret,
database: authComponent.adapter(ctx),
emailAndPassword: {
enabled: true,
disableSignUp: true,
requireEmailVerification: false,
},
plugins: [convex({ authConfig })],
} satisfies BetterAuthOptions;
};
export const createAuth = (ctx: GenericCtx<DataModel>) => {
return betterAuth(createAuthOptions(ctx));
};

View File

@@ -0,0 +1,5 @@
import { defineComponent } from "convex/server";
const component = defineComponent("betterAuth");
export default component;

31
convex/betterAuth/env.ts Normal file
View File

@@ -0,0 +1,31 @@
type BetterAuthEnv = Record<string, string | undefined>;
const schemaGenerationSecret = "convex-better-auth-schema-generation-secret";
export function getBetterAuthServerConfig(
env: BetterAuthEnv,
options: { allowMissingSecret?: boolean } = {},
) {
const secret = env.BETTER_AUTH_SECRET?.trim();
if (!secret) {
if (options.allowMissingSecret) {
return {
appUrl:
env.BETTER_AUTH_URL?.trim() ||
env.NEXT_PUBLIC_APP_URL?.trim() ||
"http://localhost:3000",
secret: schemaGenerationSecret,
};
}
throw new Error("Missing BETTER_AUTH_SECRET in the environment.");
}
return {
appUrl:
env.BETTER_AUTH_URL?.trim() ||
env.NEXT_PUBLIC_APP_URL?.trim() ||
"http://localhost:3000",
secret,
};
}

View File

@@ -0,0 +1,78 @@
/**
* This file is auto-generated. Do not edit this file manually.
* To regenerate the schema, from your project root:
*
* npx auth generate --output ./convex/betterAuth/schema.ts
*
* To customize the schema, generate to an alternate file and import
* the table definitions to your schema file. See
* https://labs.convex.dev/better-auth/features/local-install#adding-custom-indexes.
*/
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export const tables = {
user: defineTable({
name: v.string(),
email: v.string(),
emailVerified: v.boolean(),
image: v.optional(v.union(v.null(), v.string())),
createdAt: v.number(),
updatedAt: v.number(),
userId: v.optional(v.union(v.null(), v.string())),
})
.index("email_name", ["email", "name"])
.index("name", ["name"])
.index("userId", ["userId"]),
session: defineTable({
expiresAt: v.number(),
token: v.string(),
createdAt: v.number(),
updatedAt: v.number(),
ipAddress: v.optional(v.union(v.null(), v.string())),
userAgent: v.optional(v.union(v.null(), v.string())),
userId: v.string(),
})
.index("expiresAt", ["expiresAt"])
.index("expiresAt_userId", ["expiresAt", "userId"])
.index("token", ["token"])
.index("userId", ["userId"]),
account: defineTable({
accountId: v.string(),
providerId: v.string(),
userId: v.string(),
accessToken: v.optional(v.union(v.null(), v.string())),
refreshToken: v.optional(v.union(v.null(), v.string())),
idToken: v.optional(v.union(v.null(), v.string())),
accessTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
refreshTokenExpiresAt: v.optional(v.union(v.null(), v.number())),
scope: v.optional(v.union(v.null(), v.string())),
password: v.optional(v.union(v.null(), v.string())),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("accountId", ["accountId"])
.index("accountId_providerId", ["accountId", "providerId"])
.index("providerId_userId", ["providerId", "userId"])
.index("userId", ["userId"]),
verification: defineTable({
identifier: v.string(),
value: v.string(),
expiresAt: v.number(),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("expiresAt", ["expiresAt"])
.index("identifier", ["identifier"]),
jwks: defineTable({
publicKey: v.string(),
privateKey: v.string(),
createdAt: v.number(),
expiresAt: v.optional(v.union(v.null(), v.number())),
}),
};
const schema = defineSchema(tables);
export default schema;

9
convex/convex.config.ts Normal file
View File

@@ -0,0 +1,9 @@
import { defineApp } from "convex/server";
import betterAuth from "./betterAuth/convex.config";
const app = defineApp();
app.use(betterAuth);
export default app;

9
convex/http.ts Normal file
View File

@@ -0,0 +1,9 @@
import { httpRouter } from "convex/server";
import { authComponent, createAuth } from "./betterAuth/auth";
const http = httpRouter();
authComponent.registerRoutes(http, createAuth);
export default http;

View File

@@ -1,5 +1,6 @@
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
import { tables as authTables } from "./betterAuth/schema";
const campaignStatus = v.union(v.literal("active"), v.literal("paused"));
const leadPriority = v.union(
@@ -114,6 +115,7 @@ const eventDetail = v.object({
});
export default defineSchema({
...authTables,
campaigns: defineTable({
name: v.string(),
categoryMode: v.union(v.literal("preset"), v.literal("custom")),