diff --git a/packages/plugins/plugin-auth/README.md b/packages/plugins/plugin-auth/README.md index 017720da..ad5a1b17 100644 --- a/packages/plugins/plugin-auth/README.md +++ b/packages/plugins/plugin-auth/README.md @@ -2,7 +2,7 @@ Authentication & Identity Plugin for ObjectStack. -> **✨ Status:** Better-Auth library successfully integrated! Core authentication structure is in place with better-auth v1.4.18. Full API integration and advanced features are in active development. +> **✨ Status:** ObjectQL-based authentication implementation! Uses ObjectQL for data persistence (no third-party ORM required). Core authentication structure is in place with better-auth v1.4.18. ## Features @@ -12,6 +12,7 @@ Authentication & Identity Plugin for ObjectStack. - ✅ Service registration in ObjectKernel - ✅ Configuration schema support - ✅ **Better-Auth library integration (v1.4.18)** +- ✅ **ObjectQL-based database implementation (no ORM required)** - ✅ **Direct request forwarding to better-auth handler** - ✅ **Wildcard routing (`/api/v1/auth/*`)** - ✅ **Full better-auth API access via `auth.api`** @@ -28,10 +29,18 @@ Authentication & Identity Plugin for ObjectStack. - ✅ **Magic Links** - Passwordless authentication (when enabled) - ✅ **Organizations** - Multi-tenant support (when enabled) -### In Active Development -- 🔄 **Database Adapter** - Drizzle ORM integration for data persistence +### ObjectQL-Based Database Architecture +- ✅ **Native ObjectQL Data Persistence** - Uses ObjectQL's IDataEngine interface +- ✅ **No Third-Party ORM** - No dependency on drizzle-orm or other ORMs +- ✅ **Better-Auth Native Schema** - Uses better-auth's naming conventions for seamless migration +- ✅ **Object Definitions** - Auth objects defined using ObjectStack's Object Protocol + - `user` - User accounts (better-auth native table name) + - `session` - Active sessions (better-auth native table name) + - `account` - OAuth provider accounts (better-auth native table name) + - `verification` - Email/phone verification tokens (better-auth native table name) +- ✅ **ObjectQL Adapter** - Custom adapter bridges better-auth to ObjectQL -The plugin uses [better-auth](https://www.better-auth.com/) for robust, production-ready authentication functionality. All requests are forwarded directly to better-auth's universal handler, ensuring full compatibility with all better-auth features. +The plugin uses [better-auth](https://www.better-auth.com/) for robust, production-ready authentication functionality. All requests are forwarded directly to better-auth's universal handler, ensuring full compatibility with all better-auth features. Data persistence is handled by ObjectQL using **better-auth's native naming conventions** (camelCase) to ensure seamless migration for existing better-auth users. ## Installation @@ -41,18 +50,22 @@ pnpm add @objectstack/plugin-auth ## Usage -### Basic Setup +### Basic Setup with ObjectQL ```typescript import { ObjectKernel } from '@objectstack/core'; import { AuthPlugin } from '@objectstack/plugin-auth'; +import { ObjectQL } from '@objectstack/objectql'; + +// Initialize ObjectQL as the data engine +const dataEngine = new ObjectQL(); const kernel = new ObjectKernel({ plugins: [ new AuthPlugin({ secret: process.env.AUTH_SECRET, baseUrl: 'http://localhost:3000', - databaseUrl: process.env.DATABASE_URL, + // ObjectQL will be automatically injected by the kernel providers: [ { id: 'google', @@ -65,13 +78,14 @@ const kernel = new ObjectKernel({ }); ``` +**Note:** The `databaseUrl` parameter is no longer used. The plugin now uses ObjectQL's IDataEngine interface, which is provided by the kernel's `data` service. This allows the plugin to work with any ObjectQL-compatible driver (memory, SQL, NoSQL, etc.) without requiring a specific ORM. + ### With Organization Support ```typescript new AuthPlugin({ secret: process.env.AUTH_SECRET, baseUrl: 'http://localhost:3000', - databaseUrl: process.env.DATABASE_URL, plugins: { organization: true, // Enable organization/teams twoFactor: true, // Enable 2FA @@ -142,10 +156,12 @@ This package provides authentication services powered by better-auth. Current im 7. ✅ Full better-auth API support 8. ✅ OAuth providers (configurable) 9. ✅ 2FA, passkeys, magic links (configurable) -10. 🔄 Database adapter integration (in progress) +10. ✅ ObjectQL-based database implementation (no ORM required) ### Architecture +#### Request Flow + The plugin uses a **direct forwarding** approach: ```typescript @@ -164,6 +180,59 @@ This architecture provides: - ✅ **Type safety** - Full TypeScript support from better-auth - ✅ **Programmatic API** - Access auth methods via `authManager.api` +#### ObjectQL Database Architecture + +The plugin uses **ObjectQL** for data persistence instead of third-party ORMs: + +```typescript +// Object definitions use better-auth's native naming conventions +export const AuthUser = ObjectSchema.create({ + name: 'user', // better-auth native table name + fields: { + id: Field.text({ label: 'User ID', required: true }), + email: Field.email({ label: 'Email', required: true }), + emailVerified: Field.boolean({ label: 'Email Verified' }), // camelCase + name: Field.text({ label: 'Name', required: true }), + createdAt: Field.datetime({ label: 'Created At' }), // camelCase + updatedAt: Field.datetime({ label: 'Updated At' }), // camelCase + // ... other fields + }, + indexes: [ + { fields: ['email'], unique: true } + ] +}); +``` + +**Benefits:** +- ✅ **No ORM Dependencies** - No drizzle-orm, Prisma, or other ORMs required +- ✅ **Unified Data Layer** - Uses same data engine as rest of ObjectStack +- ✅ **Driver Agnostic** - Works with memory, SQL, NoSQL via ObjectQL drivers +- ✅ **Type-Safe** - Zod-based schemas provide runtime + compile-time safety +- ✅ **"Data as Code"** - Object definitions are versioned, declarative code +- ✅ **Metadata Driven** - Supports migrations, validation, indexing via metadata +- ✅ **Seamless Migration** - Uses better-auth's native naming (camelCase) for easy migration + +**Database Objects:** +Uses better-auth's native table and field names for compatibility: +- `user` - User accounts (id, email, name, emailVerified, createdAt, etc.) +- `session` - Active sessions (id, token, userId, expiresAt, ipAddress, etc.) +- `account` - OAuth provider accounts (id, providerId, accountId, userId, tokens, etc.) +- `verification` - Verification tokens (id, value, identifier, expiresAt, etc.) + +**Adapter:** +The `createObjectQLAdapter()` function bridges better-auth's database interface to ObjectQL's IDataEngine using better-auth's native naming conventions: + +```typescript +// Better-auth → ObjectQL Adapter (no name conversion needed) +const adapter = createObjectQLAdapter(dataEngine); + +// Better-auth uses this adapter for all database operations +const auth = betterAuth({ + database: adapter, + // ... other config +}); +``` + ## Development ```bash diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index 33c92c3f..491b0446 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -18,13 +18,5 @@ "@types/node": "^25.2.2", "typescript": "^5.0.0", "vitest": "^4.0.18" - }, - "peerDependencies": { - "drizzle-orm": "^0.41.0" - }, - "peerDependenciesMeta": { - "drizzle-orm": { - "optional": true - } } } diff --git a/packages/plugins/plugin-auth/src/auth-manager.ts b/packages/plugins/plugin-auth/src/auth-manager.ts index 3a4d2f6e..92f5d7cf 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.ts @@ -3,6 +3,8 @@ import { betterAuth } from 'better-auth'; import type { Auth, BetterAuthOptions } from 'better-auth'; import type { AuthConfig } from '@objectstack/spec/system'; +import type { IDataEngine } from '@objectstack/core'; +import { createObjectQLAdapter } from './objectql-adapter.js'; /** * Extended options for AuthManager @@ -13,6 +15,12 @@ export interface AuthManagerOptions extends Partial { * If not provided, one will be created from config */ authInstance?: Auth; + + /** + * ObjectQL Data Engine instance + * Required for database operations using ObjectQL instead of third-party ORMs + */ + dataEngine?: IDataEngine; } /** @@ -82,25 +90,24 @@ export class AuthManager { } /** - * Create database configuration - * TODO: Implement proper database adapter when drizzle-orm is available + * Create database configuration using ObjectQL adapter */ private createDatabaseConfig(): any { - // If databaseUrl is provided, we would use drizzle adapter - // For now, this is a placeholder configuration - if (this.config.databaseUrl) { - console.warn( - 'Database URL provided but adapter integration not yet complete. ' + - 'Install drizzle-orm and configure a proper adapter for production use.' - ); + // Use ObjectQL adapter if dataEngine is provided + if (this.config.dataEngine) { + return createObjectQLAdapter(this.config.dataEngine); } - // Return a minimal configuration that better-auth can work with - // This will need to be replaced with a proper adapter - return { - // Placeholder - will be replaced with actual adapter - adapter: 'in-memory' as any, - }; + // Fallback warning if no dataEngine is provided + console.warn( + '⚠️ WARNING: No dataEngine provided to AuthManager! ' + + 'Using in-memory storage. This is NOT suitable for production. ' + + 'Please provide a dataEngine instance (e.g., ObjectQL) in AuthManagerOptions.' + ); + + // Return a minimal in-memory configuration as fallback + // This allows the system to work in development/testing without a real database + return undefined; // better-auth will use its default in-memory adapter } /** diff --git a/packages/plugins/plugin-auth/src/auth-plugin.ts b/packages/plugins/plugin-auth/src/auth-plugin.ts index aa421406..ee909a91 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.ts @@ -67,8 +67,17 @@ export class AuthPlugin implements Plugin { throw new Error('AuthPlugin: secret is required'); } - // Initialize auth manager - this.authManager = new AuthManager(this.options); + // Get data engine service for database operations + const dataEngine = ctx.getService('data'); + if (!dataEngine) { + ctx.logger.warn('No data engine service found - auth will use in-memory storage'); + } + + // Initialize auth manager with data engine + this.authManager = new AuthManager({ + ...this.options, + dataEngine, + }); // Register auth service ctx.registerService('auth', this.authManager); diff --git a/packages/plugins/plugin-auth/src/index.ts b/packages/plugins/plugin-auth/src/index.ts index 09e94e6c..aa48cf34 100644 --- a/packages/plugins/plugin-auth/src/index.ts +++ b/packages/plugins/plugin-auth/src/index.ts @@ -5,8 +5,11 @@ * * Authentication & Identity Plugin for ObjectStack * Powered by better-auth for robust, secure authentication + * Uses ObjectQL for data persistence (no third-party ORM required) */ -export * from './auth-plugin'; -export * from './auth-manager'; +export * from './auth-plugin.js'; +export * from './auth-manager.js'; +export * from './objectql-adapter.js'; +export * from './objects/index.js'; export type { AuthConfig, AuthProviderConfig, AuthPluginConfig } from '@objectstack/spec/system'; diff --git a/packages/plugins/plugin-auth/src/objectql-adapter.ts b/packages/plugins/plugin-auth/src/objectql-adapter.ts new file mode 100644 index 00000000..0866259d --- /dev/null +++ b/packages/plugins/plugin-auth/src/objectql-adapter.ts @@ -0,0 +1,181 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import type { IDataEngine } from '@objectstack/core'; +import type { CleanedWhere } from 'better-auth/adapters'; + +/** + * ObjectQL Adapter for better-auth + * + * Bridges better-auth's database adapter interface with ObjectQL's IDataEngine. + * This allows better-auth to use ObjectQL for data persistence instead of + * third-party ORMs like drizzle-orm. + * + * Uses better-auth's native naming conventions (camelCase) for seamless migration. + * + * @param dataEngine - ObjectQL data engine instance + * @returns better-auth CustomAdapter + */ +export function createObjectQLAdapter(dataEngine: IDataEngine) { + /** + * Convert better-auth where clause to ObjectQL query format + */ + function convertWhere(where: CleanedWhere[]): Record { + const filter: Record = {}; + + for (const condition of where) { + // Use field names as-is (no conversion needed) + const fieldName = condition.field; + + if (condition.operator === 'eq') { + filter[fieldName] = condition.value; + } else if (condition.operator === 'ne') { + filter[fieldName] = { $ne: condition.value }; + } else if (condition.operator === 'in') { + filter[fieldName] = { $in: condition.value }; + } else if (condition.operator === 'gt') { + filter[fieldName] = { $gt: condition.value }; + } else if (condition.operator === 'gte') { + filter[fieldName] = { $gte: condition.value }; + } else if (condition.operator === 'lt') { + filter[fieldName] = { $lt: condition.value }; + } else if (condition.operator === 'lte') { + filter[fieldName] = { $lte: condition.value }; + } else if (condition.operator === 'contains') { + filter[fieldName] = { $regex: condition.value }; + } + } + + return filter; + } + + return { + create: async >({ model, data, select: _select }: { model: string; data: T; select?: string[] }): Promise => { + // Use model name as-is (no conversion needed) + const objectName = model; + + // Note: select parameter is currently not supported by ObjectQL's insert operation + // The full record is always returned after insertion + const result = await dataEngine.insert(objectName, data); + return result as T; + }, + + findOne: async ({ model, where, select, join: _join }: { model: string; where: CleanedWhere[]; select?: string[]; join?: any }): Promise => { + const objectName = model; + const filter = convertWhere(where); + + // Note: join parameter is not currently supported by ObjectQL's findOne operation + // Joins/populate functionality is planned for future ObjectQL releases + // For now, related data must be fetched separately + + const result = await dataEngine.findOne(objectName, { + filter, + select, + }); + + return result ? result as T : null; + }, + + findMany: async ({ model, where, limit, offset, sortBy, join: _join }: { model: string; where?: CleanedWhere[]; limit: number; offset?: number; sortBy?: { field: string; direction: 'asc' | 'desc' }; join?: any }): Promise => { + const objectName = model; + const filter = where ? convertWhere(where) : {}; + + // Note: join parameter is not currently supported by ObjectQL's find operation + // Joins/populate functionality is planned for future ObjectQL releases + + const sort = sortBy ? [{ + field: sortBy.field, + order: sortBy.direction as 'asc' | 'desc', + }] : undefined; + + const results = await dataEngine.find(objectName, { + filter, + limit: limit || 100, + skip: offset, + sort, + }); + + return results as T[]; + }, + + count: async ({ model, where }: { model: string; where?: CleanedWhere[] }): Promise => { + const objectName = model; + const filter = where ? convertWhere(where) : {}; + + return await dataEngine.count(objectName, { filter }); + }, + + update: async ({ model, where, update }: { model: string; where: CleanedWhere[]; update: Record }): Promise => { + const objectName = model; + const filter = convertWhere(where); + + // Find the record first to get its ID + const record = await dataEngine.findOne(objectName, { filter }); + if (!record) { + return null; + } + + const result = await dataEngine.update(objectName, { + ...update, + id: record.id, + }); + + return result ? result as T : null; + }, + + updateMany: async ({ model, where, update }: { model: string; where: CleanedWhere[]; update: Record }): Promise => { + const objectName = model; + const filter = convertWhere(where); + + // Note: Sequential updates are used here because ObjectQL's IDataEngine interface + // requires an ID for updates. A future optimization could use a bulk update + // operation if ObjectQL adds support for filter-based updates without IDs. + + // Find all matching records + const records = await dataEngine.find(objectName, { filter }); + + // Update each record + for (const record of records) { + await dataEngine.update(objectName, { + ...update, + id: record.id, + }); + } + + return records.length; + }, + + delete: async ({ model, where }: { model: string; where: CleanedWhere[] }): Promise => { + const objectName = model; + const filter = convertWhere(where); + + // Note: We need to find the record first to get its ID because ObjectQL's + // delete operation requires an ID. Direct filter-based delete would be more + // efficient if supported by ObjectQL in the future. + const record = await dataEngine.findOne(objectName, { filter }); + if (!record) { + return; + } + + await dataEngine.delete(objectName, { filter: { id: record.id } }); + }, + + deleteMany: async ({ model, where }: { model: string; where: CleanedWhere[] }): Promise => { + const objectName = model; + const filter = convertWhere(where); + + // Note: Sequential deletes are used here because ObjectQL's delete operation + // requires an ID in the filter. A future optimization could use a single + // delete call with the original filter if ObjectQL supports it. + + // Find all matching records + const records = await dataEngine.find(objectName, { filter }); + + // Delete each record + for (const record of records) { + await dataEngine.delete(objectName, { filter: { id: record.id } }); + } + + return records.length; + }, + }; +} diff --git a/packages/plugins/plugin-auth/src/objects/auth-account.object.ts b/packages/plugins/plugin-auth/src/objects/auth-account.object.ts new file mode 100644 index 00000000..7005a856 --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/auth-account.object.ts @@ -0,0 +1,121 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * Auth Account Object + * + * Uses better-auth's native schema for seamless migration: + * - id: string + * - createdAt: Date + * - updatedAt: Date + * - providerId: string (e.g., 'google', 'github') + * - accountId: string (provider's user ID) + * - userId: string (link to user table) + * - accessToken: string | null + * - refreshToken: string | null + * - idToken: string | null + * - accessTokenExpiresAt: Date | null + * - refreshTokenExpiresAt: Date | null + * - scope: string | null + * - password: string | null (for email/password provider) + */ +export const AuthAccount = ObjectSchema.create({ + name: 'account', + label: 'Account', + pluralLabel: 'Accounts', + icon: 'link', + description: 'OAuth and authentication provider accounts', + titleFormat: '{providerId} - {accountId}', + compactLayout: ['providerId', 'userId', 'accountId'], + + fields: { + id: Field.text({ + label: 'Account ID', + required: true, + readonly: true, + }), + + createdAt: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + updatedAt: Field.datetime({ + label: 'Updated At', + defaultValue: 'NOW()', + readonly: true, + }), + + providerId: Field.text({ + label: 'Provider ID', + required: true, + description: 'OAuth provider identifier (google, github, etc.)', + }), + + accountId: Field.text({ + label: 'Provider Account ID', + required: true, + description: "User's ID in the provider's system", + }), + + userId: Field.text({ + label: 'User ID', + required: true, + description: 'Link to user table', + }), + + accessToken: Field.textarea({ + label: 'Access Token', + required: false, + }), + + refreshToken: Field.textarea({ + label: 'Refresh Token', + required: false, + }), + + idToken: Field.textarea({ + label: 'ID Token', + required: false, + }), + + accessTokenExpiresAt: Field.datetime({ + label: 'Access Token Expires At', + required: false, + }), + + refreshTokenExpiresAt: Field.datetime({ + label: 'Refresh Token Expires At', + required: false, + }), + + scope: Field.text({ + label: 'OAuth Scope', + required: false, + }), + + password: Field.text({ + label: 'Password Hash', + required: false, + description: 'Hashed password for email/password provider', + }), + }, + + // Database indexes for performance + indexes: [ + { fields: ['userId'], unique: false }, + { fields: ['providerId', 'accountId'], unique: true }, + ], + + // Enable features + enable: { + trackHistory: false, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'list', 'create', 'update', 'delete'], + trash: true, + mru: false, + }, +}); diff --git a/packages/plugins/plugin-auth/src/objects/auth-session.object.ts b/packages/plugins/plugin-auth/src/objects/auth-session.object.ts new file mode 100644 index 00000000..25d429c4 --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/auth-session.object.ts @@ -0,0 +1,89 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * Auth Session Object + * + * Uses better-auth's native schema for seamless migration: + * - id: string + * - createdAt: Date + * - updatedAt: Date + * - userId: string + * - expiresAt: Date + * - token: string + * - ipAddress: string | null + * - userAgent: string | null + */ +export const AuthSession = ObjectSchema.create({ + name: 'session', + label: 'Session', + pluralLabel: 'Sessions', + icon: 'key', + description: 'Active user sessions', + titleFormat: 'Session {token}', + compactLayout: ['userId', 'expiresAt', 'ipAddress'], + + fields: { + id: Field.text({ + label: 'Session ID', + required: true, + readonly: true, + }), + + createdAt: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + updatedAt: Field.datetime({ + label: 'Updated At', + defaultValue: 'NOW()', + readonly: true, + }), + + userId: Field.text({ + label: 'User ID', + required: true, + }), + + expiresAt: Field.datetime({ + label: 'Expires At', + required: true, + }), + + token: Field.text({ + label: 'Session Token', + required: true, + }), + + ipAddress: Field.text({ + label: 'IP Address', + required: false, + maxLength: 45, // Support IPv6 + }), + + userAgent: Field.textarea({ + label: 'User Agent', + required: false, + }), + }, + + // Database indexes for performance + indexes: [ + { fields: ['token'], unique: true }, + { fields: ['userId'], unique: false }, + { fields: ['expiresAt'], unique: false }, + ], + + // Enable features + enable: { + trackHistory: false, // Sessions don't need history tracking + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'list', 'create', 'delete'], // No update for sessions + trash: false, // Sessions should be hard deleted + mru: false, + }, +}); diff --git a/packages/plugins/plugin-auth/src/objects/auth-user.object.ts b/packages/plugins/plugin-auth/src/objects/auth-user.object.ts new file mode 100644 index 00000000..518b3258 --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/auth-user.object.ts @@ -0,0 +1,97 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * Auth User Object + * + * Uses better-auth's native schema for seamless migration: + * - id: string + * - createdAt: Date + * - updatedAt: Date + * - email: string (unique, lowercase) + * - emailVerified: boolean + * - name: string + * - image: string | null + */ +export const AuthUser = ObjectSchema.create({ + name: 'user', + label: 'User', + pluralLabel: 'Users', + icon: 'user', + description: 'User accounts for authentication', + titleFormat: '{name} ({email})', + compactLayout: ['name', 'email', 'emailVerified'], + + fields: { + // ID is auto-generated by ObjectQL + id: Field.text({ + label: 'User ID', + required: true, + readonly: true, + }), + + createdAt: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + updatedAt: Field.datetime({ + label: 'Updated At', + defaultValue: 'NOW()', + readonly: true, + }), + + email: Field.email({ + label: 'Email', + required: true, + searchable: true, + }), + + emailVerified: Field.boolean({ + label: 'Email Verified', + defaultValue: false, + }), + + name: Field.text({ + label: 'Name', + required: true, + searchable: true, + maxLength: 255, + }), + + image: Field.url({ + label: 'Profile Image', + required: false, + }), + }, + + // Database indexes for performance + indexes: [ + { fields: ['email'], unique: true }, + { fields: ['createdAt'], unique: false }, + ], + + // Enable features + enable: { + trackHistory: true, + searchable: true, + apiEnabled: true, + apiMethods: ['get', 'list', 'create', 'update', 'delete'], + trash: true, + mru: true, + }, + + // Validation Rules + validations: [ + { + name: 'email_unique', + type: 'unique', + severity: 'error', + message: 'Email must be unique', + fields: ['email'], + caseSensitive: false, + }, + ], +}); diff --git a/packages/plugins/plugin-auth/src/objects/auth-verification.object.ts b/packages/plugins/plugin-auth/src/objects/auth-verification.object.ts new file mode 100644 index 00000000..71dfafbc --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/auth-verification.object.ts @@ -0,0 +1,78 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +/** + * Auth Verification Object + * + * Uses better-auth's native schema for seamless migration: + * - id: string + * - createdAt: Date + * - updatedAt: Date + * - value: string (verification token/code) + * - expiresAt: Date + * - identifier: string (email or phone number) + */ +export const AuthVerification = ObjectSchema.create({ + name: 'verification', + label: 'Verification', + pluralLabel: 'Verifications', + icon: 'shield-check', + description: 'Email and phone verification tokens', + titleFormat: 'Verification for {identifier}', + compactLayout: ['identifier', 'expiresAt', 'createdAt'], + + fields: { + id: Field.text({ + label: 'Verification ID', + required: true, + readonly: true, + }), + + createdAt: Field.datetime({ + label: 'Created At', + defaultValue: 'NOW()', + readonly: true, + }), + + updatedAt: Field.datetime({ + label: 'Updated At', + defaultValue: 'NOW()', + readonly: true, + }), + + value: Field.text({ + label: 'Verification Token', + required: true, + description: 'Token or code for verification', + }), + + expiresAt: Field.datetime({ + label: 'Expires At', + required: true, + }), + + identifier: Field.text({ + label: 'Identifier', + required: true, + description: 'Email address or phone number', + }), + }, + + // Database indexes for performance + indexes: [ + { fields: ['value'], unique: true }, + { fields: ['identifier'], unique: false }, + { fields: ['expiresAt'], unique: false }, + ], + + // Enable features + enable: { + trackHistory: false, + searchable: false, + apiEnabled: true, + apiMethods: ['get', 'create', 'delete'], // No list or update + trash: false, // Hard delete expired tokens + mru: false, + }, +}); diff --git a/packages/plugins/plugin-auth/src/objects/index.ts b/packages/plugins/plugin-auth/src/objects/index.ts new file mode 100644 index 00000000..273f36b9 --- /dev/null +++ b/packages/plugins/plugin-auth/src/objects/index.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Auth Objects + * + * ObjectQL-based object definitions for authentication database schema. + * These objects replace the need for third-party ORMs like drizzle-orm. + */ + +export { AuthUser } from './auth-user.object.js'; +export { AuthSession } from './auth-session.object.js'; +export { AuthAccount } from './auth-account.object.js'; +export { AuthVerification } from './auth-verification.object.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 674ad16e..ad39aff0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -609,9 +609,6 @@ importers: better-auth: specifier: ^1.4.18 version: 1.4.18(drizzle-orm@0.41.0(kysely@0.28.11))(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@25.2.2)(happy-dom@20.5.3)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.9(@types/node@25.2.2)(typescript@5.9.3))(tsx@4.21.0)) - drizzle-orm: - specifier: ^0.41.0 - version: 0.41.0(kysely@0.28.11) devDependencies: '@types/node': specifier: ^25.2.2 @@ -6854,6 +6851,7 @@ snapshots: drizzle-orm@0.41.0(kysely@0.28.11): optionalDependencies: kysely: 0.28.11 + optional: true electron-to-chromium@1.5.286: {}