mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 10:46:41 +02:00
🔀 merge: auth/complete branch with Better Auth implementation
Merged auth/complete into main with resolved conflicts: - Kept Better Auth system (EdDSA JWT via JWKS) - Removed all Coolify references - Added dev:auth and dev:chat:full scripts for auth development - Combined zitare scripts from main with auth scripts - Exported both feedback.schema and organizations.schema 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
8a43bbfc25
84 changed files with 13452 additions and 6778 deletions
|
|
@ -1,29 +0,0 @@
|
|||
import { config } from 'dotenv';
|
||||
import { migrate } from 'drizzle-orm/postgres-js/migrator';
|
||||
import { getDb, closeConnection } from './connection';
|
||||
|
||||
// Load environment variables
|
||||
config();
|
||||
|
||||
async function runMigrations() {
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
|
||||
if (!databaseUrl) {
|
||||
throw new Error('DATABASE_URL environment variable is not set');
|
||||
}
|
||||
|
||||
console.log('Running migrations...');
|
||||
|
||||
try {
|
||||
const db = getDb(databaseUrl);
|
||||
await migrate(db, { migrationsFolder: './src/db/migrations' });
|
||||
console.log('Migrations completed successfully');
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await closeConnection();
|
||||
}
|
||||
}
|
||||
|
||||
runMigrations();
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
CREATE SCHEMA "auth";
|
||||
--> statement-breakpoint
|
||||
CREATE SCHEMA "credits";
|
||||
--> statement-breakpoint
|
||||
CREATE TYPE "public"."user_role" AS ENUM('user', 'admin', 'service');--> statement-breakpoint
|
||||
CREATE TYPE "public"."transaction_status" AS ENUM('pending', 'completed', 'failed', 'cancelled');--> statement-breakpoint
|
||||
CREATE TYPE "public"."transaction_type" AS ENUM('purchase', 'usage', 'refund', 'bonus', 'expiry', 'adjustment');--> statement-breakpoint
|
||||
CREATE TABLE "auth"."accounts" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"provider" text NOT NULL,
|
||||
"provider_account_id" text NOT NULL,
|
||||
"access_token" text,
|
||||
"refresh_token" text,
|
||||
"expires_at" timestamp with time zone,
|
||||
"token_type" text,
|
||||
"scope" text,
|
||||
"id_token" text,
|
||||
"metadata" jsonb,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "auth"."passwords" (
|
||||
"user_id" uuid PRIMARY KEY NOT NULL,
|
||||
"hashed_password" text NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "auth"."security_events" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid,
|
||||
"event_type" text NOT NULL,
|
||||
"ip_address" text,
|
||||
"user_agent" text,
|
||||
"metadata" jsonb,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "auth"."sessions" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"token" text NOT NULL,
|
||||
"refresh_token" text NOT NULL,
|
||||
"refresh_token_expires_at" timestamp with time zone NOT NULL,
|
||||
"ip_address" text,
|
||||
"user_agent" text,
|
||||
"device_id" text,
|
||||
"device_name" text,
|
||||
"last_activity_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"expires_at" timestamp with time zone NOT NULL,
|
||||
"revoked_at" timestamp with time zone,
|
||||
CONSTRAINT "sessions_token_unique" UNIQUE("token"),
|
||||
CONSTRAINT "sessions_refresh_token_unique" UNIQUE("refresh_token")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "auth"."two_factor_auth" (
|
||||
"user_id" uuid PRIMARY KEY NOT NULL,
|
||||
"secret" text NOT NULL,
|
||||
"enabled" boolean DEFAULT false NOT NULL,
|
||||
"backup_codes" jsonb,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"enabled_at" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "auth"."users" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"email_verified" boolean DEFAULT false NOT NULL,
|
||||
"name" text,
|
||||
"avatar_url" text,
|
||||
"role" "user_role" DEFAULT 'user' NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deleted_at" timestamp with time zone,
|
||||
CONSTRAINT "users_email_unique" UNIQUE("email")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "auth"."verification_tokens" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"token" text NOT NULL,
|
||||
"type" text NOT NULL,
|
||||
"expires_at" timestamp with time zone NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"used_at" timestamp with time zone,
|
||||
CONSTRAINT "verification_tokens_token_unique" UNIQUE("token")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "credits"."balances" (
|
||||
"user_id" uuid PRIMARY KEY NOT NULL,
|
||||
"balance" integer DEFAULT 0 NOT NULL,
|
||||
"free_credits_remaining" integer DEFAULT 150 NOT NULL,
|
||||
"daily_free_credits" integer DEFAULT 5 NOT NULL,
|
||||
"last_daily_reset_at" timestamp with time zone DEFAULT now(),
|
||||
"total_earned" integer DEFAULT 0 NOT NULL,
|
||||
"total_spent" integer DEFAULT 0 NOT NULL,
|
||||
"version" integer DEFAULT 0 NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "credits"."packages" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"description" text,
|
||||
"credits" integer NOT NULL,
|
||||
"price_euro_cents" integer NOT NULL,
|
||||
"stripe_price_id" text,
|
||||
"active" boolean DEFAULT true NOT NULL,
|
||||
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||
"metadata" jsonb,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "packages_stripe_price_id_unique" UNIQUE("stripe_price_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "credits"."purchases" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"package_id" uuid,
|
||||
"credits" integer NOT NULL,
|
||||
"price_euro_cents" integer NOT NULL,
|
||||
"stripe_payment_intent_id" text,
|
||||
"stripe_customer_id" text,
|
||||
"status" "transaction_status" DEFAULT 'pending' NOT NULL,
|
||||
"metadata" jsonb,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"completed_at" timestamp with time zone,
|
||||
CONSTRAINT "purchases_stripe_payment_intent_id_unique" UNIQUE("stripe_payment_intent_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "credits"."transactions" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"type" "transaction_type" NOT NULL,
|
||||
"status" "transaction_status" DEFAULT 'pending' NOT NULL,
|
||||
"amount" integer NOT NULL,
|
||||
"balance_before" integer NOT NULL,
|
||||
"balance_after" integer NOT NULL,
|
||||
"app_id" text NOT NULL,
|
||||
"description" text NOT NULL,
|
||||
"metadata" jsonb,
|
||||
"idempotency_key" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"completed_at" timestamp with time zone,
|
||||
CONSTRAINT "transactions_idempotency_key_unique" UNIQUE("idempotency_key")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "credits"."usage_stats" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"app_id" text NOT NULL,
|
||||
"credits_used" integer NOT NULL,
|
||||
"date" timestamp with time zone NOT NULL,
|
||||
"metadata" jsonb
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "auth"."accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "auth"."passwords" ADD CONSTRAINT "passwords_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "auth"."security_events" ADD CONSTRAINT "security_events_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "auth"."sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "auth"."two_factor_auth" ADD CONSTRAINT "two_factor_auth_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "auth"."verification_tokens" ADD CONSTRAINT "verification_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "credits"."balances" ADD CONSTRAINT "balances_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "credits"."purchases" ADD CONSTRAINT "purchases_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "credits"."purchases" ADD CONSTRAINT "purchases_package_id_packages_id_fk" FOREIGN KEY ("package_id") REFERENCES "credits"."packages"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "credits"."transactions" ADD CONSTRAINT "transactions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "credits"."usage_stats" ADD CONSTRAINT "usage_stats_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "purchases_user_id_idx" ON "credits"."purchases" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "purchases_stripe_payment_intent_id_idx" ON "credits"."purchases" USING btree ("stripe_payment_intent_id");--> statement-breakpoint
|
||||
CREATE INDEX "transactions_user_id_idx" ON "credits"."transactions" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "transactions_app_id_idx" ON "credits"."transactions" USING btree ("app_id");--> statement-breakpoint
|
||||
CREATE INDEX "transactions_created_at_idx" ON "credits"."transactions" USING btree ("created_at");--> statement-breakpoint
|
||||
CREATE INDEX "transactions_idempotency_key_idx" ON "credits"."transactions" USING btree ("idempotency_key");--> statement-breakpoint
|
||||
CREATE INDEX "usage_stats_user_id_date_idx" ON "credits"."usage_stats" USING btree ("user_id","date");--> statement-breakpoint
|
||||
CREATE INDEX "usage_stats_app_id_date_idx" ON "credits"."usage_stats" USING btree ("app_id","date");
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1764089133415,
|
||||
"tag": "0000_lush_ironclad",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1764448681401,
|
||||
"tag": "0001_zippy_ma_gnuci",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,78 +1,83 @@
|
|||
import { pgSchema, uuid, text, timestamp, boolean, jsonb, pgEnum } from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { pgSchema, uuid, text, timestamp, boolean, jsonb, pgEnum, index } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const authSchema = pgSchema('auth');
|
||||
|
||||
// Enum for user roles
|
||||
export const userRoleEnum = pgEnum('user_role', ['user', 'admin', 'service']);
|
||||
|
||||
// Users table
|
||||
// Users table (Better Auth schema)
|
||||
export const users = authSchema.table('users', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
id: text('id').primaryKey(), // Better Auth generates nanoid
|
||||
name: text('name').notNull(),
|
||||
email: text('email').unique().notNull(),
|
||||
emailVerified: boolean('email_verified').default(false).notNull(),
|
||||
name: text('name'),
|
||||
avatarUrl: text('avatar_url'),
|
||||
role: userRoleEnum('role').default('user').notNull(),
|
||||
image: text('image'), // Better Auth uses 'image' not 'avatarUrl'
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
// Custom fields (not required by Better Auth)
|
||||
role: userRoleEnum('role').default('user').notNull(),
|
||||
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
||||
});
|
||||
|
||||
// Sessions table
|
||||
// Sessions table (Better Auth schema)
|
||||
export const sessions = authSchema.table('sessions', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
id: text('id').primaryKey(), // Better Auth generates nanoid
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
token: text('token').unique().notNull(),
|
||||
refreshToken: text('refresh_token').unique().notNull(),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
// Custom fields (not required by Better Auth)
|
||||
refreshToken: text('refresh_token').unique(),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
|
||||
deviceId: text('device_id'),
|
||||
deviceName: text('device_name'),
|
||||
lastActivityAt: timestamp('last_activity_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
lastActivityAt: timestamp('last_activity_at', { withTimezone: true }).defaultNow(),
|
||||
revokedAt: timestamp('revoked_at', { withTimezone: true }),
|
||||
});
|
||||
|
||||
// Accounts table (for OAuth providers)
|
||||
// Accounts table (for OAuth providers and credentials - Better Auth schema)
|
||||
export const accounts = authSchema.table('accounts', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
id: text('id').primaryKey(), // Better Auth generates nanoid
|
||||
accountId: text('account_id').notNull(), // Better Auth field
|
||||
providerId: text('provider_id').notNull(), // Better Auth field (was 'provider')
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
provider: text('provider').notNull(), // 'google', 'github', 'apple', etc.
|
||||
providerAccountId: text('provider_account_id').notNull(),
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }),
|
||||
tokenType: text('token_type'),
|
||||
scope: text('scope'),
|
||||
idToken: text('id_token'),
|
||||
metadata: jsonb('metadata'),
|
||||
accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
|
||||
scope: text('scope'),
|
||||
password: text('password'), // Better Auth stores hashed password here for credential provider
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Verification tokens (for email verification, password reset)
|
||||
export const verificationTokens = authSchema.table('verification_tokens', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
token: text('token').unique().notNull(),
|
||||
type: text('type').notNull(), // 'email_verification', 'password_reset'
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
usedAt: timestamp('used_at', { withTimezone: true }),
|
||||
});
|
||||
// Verification table (Better Auth schema - for email verification, password reset)
|
||||
export const verificationTokens = authSchema.table(
|
||||
'verification',
|
||||
{
|
||||
id: text('id').primaryKey(), // Better Auth generates nanoid
|
||||
identifier: text('identifier').notNull(), // Better Auth uses identifier (e.g., email)
|
||||
value: text('value').notNull(), // Better Auth uses value (the token)
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
identifierIdx: index('verification_identifier_idx').on(table.identifier),
|
||||
})
|
||||
);
|
||||
|
||||
// Password table (separate for security)
|
||||
export const passwords = authSchema.table('passwords', {
|
||||
userId: uuid('user_id')
|
||||
userId: text('user_id')
|
||||
.primaryKey()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
hashedPassword: text('hashed_password').notNull(),
|
||||
|
|
@ -82,23 +87,31 @@ export const passwords = authSchema.table('passwords', {
|
|||
|
||||
// Two-factor authentication
|
||||
export const twoFactorAuth = authSchema.table('two_factor_auth', {
|
||||
userId: uuid('user_id')
|
||||
userId: text('user_id')
|
||||
.primaryKey()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
secret: text('secret').notNull(),
|
||||
enabled: boolean('enabled').default(false).notNull(),
|
||||
backupCodes: jsonb('backup_codes'), // Array of hashed backup codes
|
||||
backupCodes: jsonb('backup_codes'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
enabledAt: timestamp('enabled_at', { withTimezone: true }),
|
||||
});
|
||||
|
||||
// Security events log
|
||||
export const securityEvents = authSchema.table('security_events', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }),
|
||||
eventType: text('event_type').notNull(), // 'login', 'logout', 'password_reset', 'suspicious_activity'
|
||||
id: uuid('id').primaryKey().defaultRandom(), // Our table, can keep UUID
|
||||
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }),
|
||||
eventType: text('event_type').notNull(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
metadata: jsonb('metadata'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// JWKS table (Better Auth JWT plugin - stores signing keys)
|
||||
export const jwks = authSchema.table('jwks', {
|
||||
id: text('id').primaryKey(),
|
||||
publicKey: text('public_key').notNull(),
|
||||
privateKey: text('private_key').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
boolean,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { users } from './auth.schema';
|
||||
import { organizations } from './organizations.schema';
|
||||
|
||||
export const creditsSchema = pgSchema('credits');
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ export const transactionStatusEnum = pgEnum('transaction_status', [
|
|||
|
||||
// Credit balances (one per user)
|
||||
export const balances = creditsSchema.table('balances', {
|
||||
userId: uuid('user_id')
|
||||
userId: text('user_id')
|
||||
.primaryKey()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
balance: integer('balance').default(0).notNull(),
|
||||
|
|
@ -42,7 +43,7 @@ export const balances = creditsSchema.table('balances', {
|
|||
lastDailyResetAt: timestamp('last_daily_reset_at', { withTimezone: true }).defaultNow(),
|
||||
totalEarned: integer('total_earned').default(0).notNull(),
|
||||
totalSpent: integer('total_spent').default(0).notNull(),
|
||||
version: integer('version').default(0).notNull(), // For optimistic locking
|
||||
version: integer('version').default(0).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
|
@ -52,7 +53,7 @@ export const transactions = creditsSchema.table(
|
|||
'transactions',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
type: transactionTypeEnum('type').notNull(),
|
||||
|
|
@ -60,9 +61,10 @@ export const transactions = creditsSchema.table(
|
|||
amount: integer('amount').notNull(),
|
||||
balanceBefore: integer('balance_before').notNull(),
|
||||
balanceAfter: integer('balance_after').notNull(),
|
||||
appId: text('app_id').notNull(), // 'memoro', 'chat', 'picture', etc.
|
||||
appId: text('app_id').notNull(),
|
||||
description: text('description').notNull(),
|
||||
metadata: jsonb('metadata'), // Additional context
|
||||
organizationId: text('organization_id').references(() => organizations.id),
|
||||
metadata: jsonb('metadata'),
|
||||
idempotencyKey: text('idempotency_key').unique(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
completedAt: timestamp('completed_at', { withTimezone: true }),
|
||||
|
|
@ -70,6 +72,7 @@ export const transactions = creditsSchema.table(
|
|||
(table) => ({
|
||||
userIdIdx: index('transactions_user_id_idx').on(table.userId),
|
||||
appIdIdx: index('transactions_app_id_idx').on(table.appId),
|
||||
organizationIdIdx: index('transactions_organization_id_idx').on(table.organizationId),
|
||||
createdAtIdx: index('transactions_created_at_idx').on(table.createdAt),
|
||||
idempotencyKeyIdx: index('transactions_idempotency_key_idx').on(table.idempotencyKey),
|
||||
})
|
||||
|
|
@ -80,8 +83,8 @@ export const packages = creditsSchema.table('packages', {
|
|||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
credits: integer('credits').notNull(), // Number of credits
|
||||
priceEuroCents: integer('price_euro_cents').notNull(), // Price in euro cents
|
||||
credits: integer('credits').notNull(),
|
||||
priceEuroCents: integer('price_euro_cents').notNull(),
|
||||
stripePriceId: text('stripe_price_id').unique(),
|
||||
active: boolean('active').default(true).notNull(),
|
||||
sortOrder: integer('sort_order').default(0).notNull(),
|
||||
|
|
@ -95,7 +98,7 @@ export const purchases = creditsSchema.table(
|
|||
'purchases',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
packageId: uuid('package_id').references(() => packages.id),
|
||||
|
|
@ -121,7 +124,7 @@ export const usageStats = creditsSchema.table(
|
|||
'usage_stats',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
appId: text('app_id').notNull(),
|
||||
|
|
@ -134,3 +137,47 @@ export const usageStats = creditsSchema.table(
|
|||
appIdDateIdx: index('usage_stats_app_id_date_idx').on(table.appId, table.date),
|
||||
})
|
||||
);
|
||||
|
||||
// Organization credit balances (B2B)
|
||||
export const organizationBalances = creditsSchema.table('organization_balances', {
|
||||
organizationId: text('organization_id')
|
||||
.primaryKey()
|
||||
.references(() => organizations.id, { onDelete: 'cascade' }),
|
||||
balance: integer('balance').default(0).notNull(),
|
||||
allocatedCredits: integer('allocated_credits').default(0).notNull(),
|
||||
availableCredits: integer('available_credits').default(0).notNull(),
|
||||
totalPurchased: integer('total_purchased').default(0).notNull(),
|
||||
totalAllocated: integer('total_allocated').default(0).notNull(),
|
||||
version: integer('version').default(0).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Credit allocations (B2B - tracking allocations from org to employees)
|
||||
export const creditAllocations = creditsSchema.table(
|
||||
'credit_allocations',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
organizationId: text('organization_id')
|
||||
.references(() => organizations.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
employeeId: text('employee_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
amount: integer('amount').notNull(),
|
||||
allocatedBy: text('allocated_by')
|
||||
.references(() => users.id)
|
||||
.notNull(),
|
||||
reason: text('reason'),
|
||||
balanceBefore: integer('balance_before').notNull(),
|
||||
balanceAfter: integer('balance_after').notNull(),
|
||||
metadata: jsonb('metadata'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
organizationIdIdx: index('credit_allocations_organization_id_idx').on(table.organizationId),
|
||||
employeeIdIdx: index('credit_allocations_employee_id_idx').on(table.employeeId),
|
||||
allocatedByIdx: index('credit_allocations_allocated_by_idx').on(table.allocatedBy),
|
||||
createdAtIdx: index('credit_allocations_created_at_idx').on(table.createdAt),
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './auth.schema';
|
||||
export * from './credits.schema';
|
||||
export * from './feedback.schema';
|
||||
export * from './organizations.schema';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { pgSchema, text, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
|
||||
import { authSchema, users } from './auth.schema';
|
||||
|
||||
/**
|
||||
* Better Auth Organization Tables
|
||||
* These tables follow Better Auth's organization plugin schema requirements
|
||||
* @see https://www.better-auth.com/docs/plugins/organization
|
||||
*
|
||||
* Note: Better Auth uses TEXT for IDs (nanoid/ULID), but we use UUID for users.
|
||||
* The foreign key constraints will be added via raw SQL migration to handle the type difference.
|
||||
*/
|
||||
|
||||
// Organizations table
|
||||
export const organizations = authSchema.table(
|
||||
'organizations',
|
||||
{
|
||||
id: text('id').primaryKey(), // Better Auth uses TEXT IDs (ULIDs/nanoids)
|
||||
name: text('name').notNull(),
|
||||
slug: text('slug').unique(),
|
||||
logo: text('logo'),
|
||||
metadata: jsonb('metadata'), // Additional organization data
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
slugIdx: index('organizations_slug_idx').on(table.slug),
|
||||
})
|
||||
);
|
||||
|
||||
// Members table (links users to organizations with roles)
|
||||
export const members = authSchema.table(
|
||||
'members',
|
||||
{
|
||||
id: text('id').primaryKey(), // Better Auth uses TEXT IDs
|
||||
organizationId: text('organization_id')
|
||||
.references(() => organizations.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
userId: text('user_id').notNull(), // References auth.users.id (UUID cast to TEXT)
|
||||
role: text('role').notNull(), // 'owner', 'admin', 'member', or custom roles
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
organizationIdIdx: index('members_organization_id_idx').on(table.organizationId),
|
||||
userIdIdx: index('members_user_id_idx').on(table.userId),
|
||||
organizationUserIdx: index('members_organization_user_idx').on(
|
||||
table.organizationId,
|
||||
table.userId
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
// Invitations table (for inviting users to organizations)
|
||||
export const invitations = authSchema.table(
|
||||
'invitations',
|
||||
{
|
||||
id: text('id').primaryKey(), // Better Auth uses TEXT IDs
|
||||
organizationId: text('organization_id')
|
||||
.references(() => organizations.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
email: text('email').notNull(),
|
||||
role: text('role').notNull(), // Role they'll have when they accept
|
||||
status: text('status').notNull(), // 'pending', 'accepted', 'rejected', 'canceled'
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
inviterId: text('inviter_id'), // References auth.users.id (UUID cast to TEXT)
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
organizationIdIdx: index('invitations_organization_id_idx').on(table.organizationId),
|
||||
emailIdx: index('invitations_email_idx').on(table.email),
|
||||
statusIdx: index('invitations_status_idx').on(table.status),
|
||||
})
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue