feat: enhance auth schema and add CLI configuration

- Updated the user schema to include new fields: banned, banReason, and banExpires for improved user management.
- Introduced a new auth-cli configuration file to facilitate database schema generation and inspection for better-auth plugins.
- Ensured the CLI configuration mirrors the plugin set in auth.ts for consistency across the application.
This commit is contained in:
Mauricio Siu
2026-04-19 12:05:37 -06:00
parent f06c9deddf
commit 0dbd1039e8
2 changed files with 300 additions and 236 deletions

View File

@@ -1,299 +1,311 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { import {
boolean, pgTable,
index, text,
integer, timestamp,
pgTable, boolean,
text, integer,
timestamp, index,
uniqueIndex, uniqueIndex,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
export const user = pgTable("user", { export const user = pgTable("user", {
id: text("id").primaryKey(), id: text("id").primaryKey(),
firstName: text("first_name").notNull(), firstName: text("first_name").notNull(),
email: text("email").notNull().unique(), email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").default(false).notNull(), emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"), image: text("image"),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at") updatedAt: timestamp("updated_at")
.defaultNow() .defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
twoFactorEnabled: boolean("two_factor_enabled").default(false), twoFactorEnabled: boolean("two_factor_enabled").default(false),
role: text("role"), role: text("role"),
ownerId: text("owner_id"), banned: boolean("banned").default(false),
allowImpersonation: boolean("allow_impersonation").default(false), banReason: text("ban_reason"),
lastName: text("last_name").default(""), banExpires: timestamp("ban_expires"),
enableEnterpriseFeatures: boolean("enable_enterprise_features"), ownerId: text("owner_id"),
isValidEnterpriseLicense: boolean("is_valid_enterprise_license"), allowImpersonation: boolean("allow_impersonation").default(false),
lastName: text("last_name").default(""),
enableEnterpriseFeatures: boolean("enable_enterprise_features"),
isValidEnterpriseLicense: boolean("is_valid_enterprise_license"),
}); });
export const session = pgTable( export const session = pgTable(
"session", "session",
{ {
id: text("id").primaryKey(), id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(), expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(), token: text("token").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at") updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
ipAddress: text("ip_address"), ipAddress: text("ip_address"),
userAgent: text("user_agent"), userAgent: text("user_agent"),
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => user.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
activeOrganizationId: text("active_organization_id"), activeOrganizationId: text("active_organization_id"),
}, impersonatedBy: text("impersonated_by"),
(table) => [index("session_userId_idx").on(table.userId)], },
(table) => [index("session_userId_idx").on(table.userId)],
); );
export const account = pgTable( export const account = pgTable(
"account", "account",
{ {
id: text("id").primaryKey(), id: text("id").primaryKey(),
accountId: text("account_id").notNull(), accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(), providerId: text("provider_id").notNull(),
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => user.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"), accessToken: text("access_token"),
refreshToken: text("refresh_token"), refreshToken: text("refresh_token"),
idToken: text("id_token"), idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"), accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"), scope: text("scope"),
password: text("password"), password: text("password"),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at") updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
}, },
(table) => [index("account_userId_idx").on(table.userId)], (table) => [index("account_userId_idx").on(table.userId)],
); );
export const verification = pgTable( export const verification = pgTable(
"verification", "verification",
{ {
id: text("id").primaryKey(), id: text("id").primaryKey(),
identifier: text("identifier").notNull(), identifier: text("identifier").notNull(),
value: text("value").notNull(), value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(), expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at") updatedAt: timestamp("updated_at")
.defaultNow() .defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
}, },
(table) => [index("verification_identifier_idx").on(table.identifier)], (table) => [index("verification_identifier_idx").on(table.identifier)],
); );
export const apikey = pgTable( export const apikey = pgTable(
"apikey", "apikey",
{ {
id: text("id").primaryKey(), id: text("id").primaryKey(),
configId: text("config_id").default("default").notNull(), configId: text("config_id").default("default").notNull(),
name: text("name"), name: text("name"),
start: text("start"), start: text("start"),
referenceId: text("reference_id").notNull(), referenceId: text("reference_id").notNull(),
prefix: text("prefix"), prefix: text("prefix"),
key: text("key").notNull(), key: text("key").notNull(),
refillInterval: integer("refill_interval"), refillInterval: integer("refill_interval"),
refillAmount: integer("refill_amount"), refillAmount: integer("refill_amount"),
lastRefillAt: timestamp("last_refill_at"), lastRefillAt: timestamp("last_refill_at"),
enabled: boolean("enabled").default(true), enabled: boolean("enabled").default(true),
rateLimitEnabled: boolean("rate_limit_enabled").default(true), rateLimitEnabled: boolean("rate_limit_enabled").default(true),
rateLimitTimeWindow: integer("rate_limit_time_window").default(86400000), rateLimitTimeWindow: integer("rate_limit_time_window").default(86400000),
rateLimitMax: integer("rate_limit_max").default(10), rateLimitMax: integer("rate_limit_max").default(10),
requestCount: integer("request_count").default(0), requestCount: integer("request_count").default(0),
remaining: integer("remaining"), remaining: integer("remaining"),
lastRequest: timestamp("last_request"), lastRequest: timestamp("last_request"),
expiresAt: timestamp("expires_at"), expiresAt: timestamp("expires_at"),
createdAt: timestamp("created_at").notNull(), createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(), updatedAt: timestamp("updated_at").notNull(),
permissions: text("permissions"), permissions: text("permissions"),
metadata: text("metadata"), metadata: text("metadata"),
}, },
(table) => [ (table) => [
index("apikey_configId_idx").on(table.configId), index("apikey_configId_idx").on(table.configId),
index("apikey_referenceId_idx").on(table.referenceId), index("apikey_referenceId_idx").on(table.referenceId),
index("apikey_key_idx").on(table.key), index("apikey_key_idx").on(table.key),
], ],
); );
export const ssoProvider = pgTable("sso_provider", { export const ssoProvider = pgTable("sso_provider", {
id: text("id").primaryKey(), id: text("id").primaryKey(),
issuer: text("issuer").notNull(), issuer: text("issuer").notNull(),
oidcConfig: text("oidc_config"), oidcConfig: text("oidc_config"),
samlConfig: text("saml_config"), samlConfig: text("saml_config"),
userId: text("user_id").references(() => user.id, { onDelete: "cascade" }), userId: text("user_id").references(() => user.id, { onDelete: "cascade" }),
providerId: text("provider_id").notNull().unique(), providerId: text("provider_id").notNull().unique(),
organizationId: text("organization_id"), organizationId: text("organization_id"),
domain: text("domain").notNull(), domain: text("domain").notNull(),
}); });
export const twoFactor = pgTable( export const twoFactor = pgTable(
"two_factor", "two_factor",
{ {
id: text("id").primaryKey(), id: text("id").primaryKey(),
secret: text("secret").notNull(), secret: text("secret").notNull(),
backupCodes: text("backup_codes").notNull(), backupCodes: text("backup_codes").notNull(),
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => user.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
}, verified: boolean("verified").default(true),
(table) => [ },
index("twoFactor_secret_idx").on(table.secret), (table) => [
index("twoFactor_userId_idx").on(table.userId), index("twoFactor_secret_idx").on(table.secret),
], index("twoFactor_userId_idx").on(table.userId),
],
); );
export const organization = pgTable( export const organization = pgTable(
"organization", "organization",
{ {
id: text("id").primaryKey(), id: text("id").primaryKey(),
name: text("name").notNull(), name: text("name").notNull(),
slug: text("slug").notNull().unique(), slug: text("slug").notNull().unique(),
logo: text("logo"), logo: text("logo"),
createdAt: timestamp("created_at").notNull(), createdAt: timestamp("created_at").notNull(),
metadata: text("metadata"), metadata: text("metadata"),
}, },
(table) => [uniqueIndex("organization_slug_uidx").on(table.slug)], (table) => [uniqueIndex("organization_slug_uidx").on(table.slug)],
); );
export const organizationRole = pgTable( export const organizationRole = pgTable(
"organization_role", "organization_role",
{ {
id: text("id").primaryKey(), id: text("id").primaryKey(),
organizationId: text("organization_id") organizationId: text("organization_id")
.notNull() .notNull()
.references(() => organization.id, { onDelete: "cascade" }), .references(() => organization.id, { onDelete: "cascade" }),
role: text("role").notNull(), role: text("role").notNull(),
permission: text("permission").notNull(), permission: text("permission").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").$onUpdate( updatedAt: timestamp("updated_at").$onUpdate(
() => /* @__PURE__ */ new Date(), () => /* @__PURE__ */ new Date(),
), ),
}, },
(table) => [ (table) => [
index("organizationRole_organizationId_idx").on(table.organizationId), index("organizationRole_organizationId_idx").on(table.organizationId),
index("organizationRole_role_idx").on(table.role), index("organizationRole_role_idx").on(table.role),
], ],
); );
export const member = pgTable( export const member = pgTable(
"member", "member",
{ {
id: text("id").primaryKey(), id: text("id").primaryKey(),
organizationId: text("organization_id") organizationId: text("organization_id")
.notNull() .notNull()
.references(() => organization.id, { onDelete: "cascade" }), .references(() => organization.id, { onDelete: "cascade" }),
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => user.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
role: text("role").default("member").notNull(), role: text("role").default("member").notNull(),
createdAt: timestamp("created_at").notNull(), createdAt: timestamp("created_at").notNull(),
}, },
(table) => [ (table) => [
index("member_organizationId_idx").on(table.organizationId), index("member_organizationId_idx").on(table.organizationId),
index("member_userId_idx").on(table.userId), index("member_userId_idx").on(table.userId),
], ],
); );
export const invitation = pgTable( export const invitation = pgTable(
"invitation", "invitation",
{ {
id: text("id").primaryKey(), id: text("id").primaryKey(),
organizationId: text("organization_id") organizationId: text("organization_id")
.notNull() .notNull()
.references(() => organization.id, { onDelete: "cascade" }), .references(() => organization.id, { onDelete: "cascade" }),
email: text("email").notNull(), email: text("email").notNull(),
role: text("role"), role: text("role"),
status: text("status").default("pending").notNull(), status: text("status").default("pending").notNull(),
expiresAt: timestamp("expires_at").notNull(), expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),
inviterId: text("inviter_id") inviterId: text("inviter_id")
.notNull() .notNull()
.references(() => user.id, { onDelete: "cascade" }), .references(() => user.id, { onDelete: "cascade" }),
}, },
(table) => [ (table) => [
index("invitation_organizationId_idx").on(table.organizationId), index("invitation_organizationId_idx").on(table.organizationId),
index("invitation_email_idx").on(table.email), index("invitation_email_idx").on(table.email),
], ],
); );
export const scimProvider = pgTable("scim_provider", {
id: text("id").primaryKey(),
providerId: text("provider_id").notNull().unique(),
scimToken: text("scim_token").notNull().unique(),
organizationId: text("organization_id"),
});
export const userRelations = relations(user, ({ many }) => ({ export const userRelations = relations(user, ({ many }) => ({
sessions: many(session), sessions: many(session),
accounts: many(account), accounts: many(account),
ssoProviders: many(ssoProvider), ssoProviders: many(ssoProvider),
twoFactors: many(twoFactor), twoFactors: many(twoFactor),
members: many(member), members: many(member),
invitations: many(invitation), invitations: many(invitation),
})); }));
export const sessionRelations = relations(session, ({ one }) => ({ export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, { user: one(user, {
fields: [session.userId], fields: [session.userId],
references: [user.id], references: [user.id],
}), }),
})); }));
export const accountRelations = relations(account, ({ one }) => ({ export const accountRelations = relations(account, ({ one }) => ({
user: one(user, { user: one(user, {
fields: [account.userId], fields: [account.userId],
references: [user.id], references: [user.id],
}), }),
})); }));
export const ssoProviderRelations = relations(ssoProvider, ({ one }) => ({ export const ssoProviderRelations = relations(ssoProvider, ({ one }) => ({
user: one(user, { user: one(user, {
fields: [ssoProvider.userId], fields: [ssoProvider.userId],
references: [user.id], references: [user.id],
}), }),
})); }));
export const twoFactorRelations = relations(twoFactor, ({ one }) => ({ export const twoFactorRelations = relations(twoFactor, ({ one }) => ({
user: one(user, { user: one(user, {
fields: [twoFactor.userId], fields: [twoFactor.userId],
references: [user.id], references: [user.id],
}), }),
})); }));
export const organizationRelations = relations(organization, ({ many }) => ({ export const organizationRelations = relations(organization, ({ many }) => ({
organizationRoles: many(organizationRole), organizationRoles: many(organizationRole),
members: many(member), members: many(member),
invitations: many(invitation), invitations: many(invitation),
})); }));
export const organizationRoleRelations = relations( export const organizationRoleRelations = relations(
organizationRole, organizationRole,
({ one }) => ({ ({ one }) => ({
organization: one(organization, { organization: one(organization, {
fields: [organizationRole.organizationId], fields: [organizationRole.organizationId],
references: [organization.id], references: [organization.id],
}), }),
}), }),
); );
export const memberRelations = relations(member, ({ one }) => ({ export const memberRelations = relations(member, ({ one }) => ({
organization: one(organization, { organization: one(organization, {
fields: [member.organizationId], fields: [member.organizationId],
references: [organization.id], references: [organization.id],
}), }),
user: one(user, { user: one(user, {
fields: [member.userId], fields: [member.userId],
references: [user.id], references: [user.id],
}), }),
})); }));
export const invitationRelations = relations(invitation, ({ one }) => ({ export const invitationRelations = relations(invitation, ({ one }) => ({
organization: one(organization, { organization: one(organization, {
fields: [invitation.organizationId], fields: [invitation.organizationId],
references: [organization.id], references: [organization.id],
}), }),
user: one(user, { user: one(user, {
fields: [invitation.inviterId], fields: [invitation.inviterId],
references: [user.id], references: [user.id],
}), }),
})); }));

View File

@@ -0,0 +1,52 @@
import { apiKey } from "@better-auth/api-key";
import { scim } from "@better-auth/scim";
import { sso } from "@better-auth/sso";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { admin, organization, twoFactor } from "better-auth/plugins";
import { db } from "../db";
import * as schema from "../db/schema";
import { ac, adminRole, memberRole, ownerRole } from "./access-control";
/**
* Minimal better-auth config used only by `@better-auth/cli` to generate /
* inspect database schemas. Must mirror the plugin set in `auth.ts` so the CLI
* sees every table each plugin expects.
*
* Do NOT import this file from the runtime — use `auth.ts` for that.
*/
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema,
}),
user: {
modelName: "user",
fields: {
name: "firstName",
},
additionalFields: {
role: { type: "string", input: false },
ownerId: { type: "string", input: false },
allowImpersonation: { type: "boolean", defaultValue: false },
lastName: { type: "string", required: false, defaultValue: "" },
enableEnterpriseFeatures: { type: "boolean", required: false },
isValidEnterpriseLicense: { type: "boolean", required: false },
},
},
plugins: [
apiKey({ enableMetadata: true, references: "user" }),
sso(),
twoFactor(),
organization({
ac,
roles: { owner: ownerRole, admin: adminRole, member: memberRole },
dynamicAccessControl: {
enabled: true,
maximumRolesPerOrganization: 10,
},
}),
scim(),
admin(),
],
});