@rebasepro/server-postgres 0.0.1-canary.4829d6e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +86 -0
- package/dist/PostgresAdapter.d.ts +6 -0
- package/dist/PostgresBackendDriver.d.ts +150 -0
- package/dist/PostgresBootstrapper.d.ts +51 -0
- package/dist/auth/ensure-tables.d.ts +10 -0
- package/dist/auth/services.d.ts +250 -0
- package/dist/backup/backup-cli.d.ts +3 -0
- package/dist/backup/backup-cron.d.ts +53 -0
- package/dist/backup/backup-service.d.ts +85 -0
- package/dist/backup/index.d.ts +12 -0
- package/dist/backup/pg-tools.d.ts +110 -0
- package/dist/backup/retention.d.ts +35 -0
- package/dist/chunk-DSJWtz9O.js +40 -0
- package/dist/cli-errors.d.ts +42 -0
- package/dist/cli-helpers.d.ts +7 -0
- package/dist/cli.d.ts +1 -0
- package/dist/collections/PostgresCollectionRegistry.d.ts +47 -0
- package/dist/connection.d.ts +65 -0
- package/dist/data-transformer.d.ts +55 -0
- package/dist/databasePoolManager.d.ts +20 -0
- package/dist/history/HistoryService.d.ts +71 -0
- package/dist/history/ensure-history-table.d.ts +7 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.es.js +23535 -0
- package/dist/index.es.js.map +1 -0
- package/dist/interfaces.d.ts +18 -0
- package/dist/schema/auth-bootstrap-sql.d.ts +24 -0
- package/dist/schema/auth-default-policies.d.ts +10 -0
- package/dist/schema/auth-schema.d.ts +2376 -0
- package/dist/schema/doctor-cli.d.ts +2 -0
- package/dist/schema/doctor.d.ts +58 -0
- package/dist/schema/dynamic-tables.d.ts +31 -0
- package/dist/schema/generate-drizzle-schema-logic.d.ts +2 -0
- package/dist/schema/generate-drizzle-schema.d.ts +1 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -0
- package/dist/schema/generate-postgres-ddl.d.ts +1 -0
- package/dist/schema/introspect-db-inference.d.ts +5 -0
- package/dist/schema/introspect-db-logic.d.ts +118 -0
- package/dist/schema/introspect-db.d.ts +1 -0
- package/dist/schema/introspect-runtime.d.ts +57 -0
- package/dist/schema/test-schema.d.ts +24 -0
- package/dist/security/policy-drift.d.ts +57 -0
- package/dist/security/rls-enforcement.d.ts +122 -0
- package/dist/services/BranchService.d.ts +47 -0
- package/dist/services/FetchService.d.ts +214 -0
- package/dist/services/PersistService.d.ts +39 -0
- package/dist/services/RelationService.d.ts +109 -0
- package/dist/services/cdc/CdcListener.d.ts +54 -0
- package/dist/services/cdc/trigger-cdc.d.ts +64 -0
- package/dist/services/collection-helpers.d.ts +38 -0
- package/dist/services/dataService.d.ts +110 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/realtimeService.d.ts +298 -0
- package/dist/src-Eh-CZosp.js +595 -0
- package/dist/src-Eh-CZosp.js.map +1 -0
- package/dist/types.d.ts +3 -0
- package/dist/utils/drizzle-conditions.d.ts +138 -0
- package/dist/utils/pg-array-null-patch.d.ts +16 -0
- package/dist/utils/pg-error-utils.d.ts +65 -0
- package/dist/utils/table-classification.d.ts +8 -0
- package/dist/websocket.d.ts +18 -0
- package/package.json +113 -0
- package/src/PostgresAdapter.ts +58 -0
- package/src/PostgresBackendDriver.ts +1387 -0
- package/src/PostgresBootstrapper.ts +581 -0
- package/src/auth/ensure-tables.ts +367 -0
- package/src/auth/services.ts +1321 -0
- package/src/backup/backup-cli.ts +383 -0
- package/src/backup/backup-cron.ts +189 -0
- package/src/backup/backup-service.ts +299 -0
- package/src/backup/index.ts +12 -0
- package/src/backup/pg-tools.ts +231 -0
- package/src/backup/retention.ts +75 -0
- package/src/cli-errors.ts +265 -0
- package/src/cli-helpers.ts +196 -0
- package/src/cli.ts +786 -0
- package/src/collections/PostgresCollectionRegistry.ts +103 -0
- package/src/connection.ts +166 -0
- package/src/data-transformer.ts +733 -0
- package/src/databasePoolManager.ts +87 -0
- package/src/history/HistoryService.ts +272 -0
- package/src/history/ensure-history-table.ts +46 -0
- package/src/index.ts +15 -0
- package/src/interfaces.ts +60 -0
- package/src/schema/auth-bootstrap-sql.ts +41 -0
- package/src/schema/auth-default-policies.ts +125 -0
- package/src/schema/auth-schema.ts +233 -0
- package/src/schema/doctor-cli.ts +113 -0
- package/src/schema/doctor.ts +733 -0
- package/src/schema/dynamic-tables.test.ts +302 -0
- package/src/schema/dynamic-tables.ts +293 -0
- package/src/schema/generate-drizzle-schema-logic.ts +850 -0
- package/src/schema/generate-drizzle-schema.ts +131 -0
- package/src/schema/generate-postgres-ddl-logic.ts +490 -0
- package/src/schema/generate-postgres-ddl.ts +92 -0
- package/src/schema/introspect-db-inference.ts +238 -0
- package/src/schema/introspect-db-logic.ts +910 -0
- package/src/schema/introspect-db.ts +266 -0
- package/src/schema/introspect-runtime.test.ts +212 -0
- package/src/schema/introspect-runtime.ts +293 -0
- package/src/schema/test-schema.ts +11 -0
- package/src/security/policy-drift.test.ts +122 -0
- package/src/security/policy-drift.ts +159 -0
- package/src/security/rls-enforcement.ts +295 -0
- package/src/services/BranchService.ts +251 -0
- package/src/services/FetchService.ts +1661 -0
- package/src/services/PersistService.ts +329 -0
- package/src/services/RelationService.ts +1306 -0
- package/src/services/cdc/CdcListener.ts +167 -0
- package/src/services/cdc/trigger-cdc.ts +169 -0
- package/src/services/collection-helpers.ts +151 -0
- package/src/services/dataService.ts +246 -0
- package/src/services/index.ts +13 -0
- package/src/services/realtimeService.ts +1502 -0
- package/src/types.ts +4 -0
- package/src/utils/drizzle-conditions.ts +1162 -0
- package/src/utils/pg-array-null-patch.ts +42 -0
- package/src/utils/pg-error-utils.ts +227 -0
- package/src/utils/table-classification.ts +16 -0
- package/src/websocket.ts +640 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
+
import { logger } from "@rebasepro/server";
|
|
4
|
+
import type { CollectionConfig } from "@rebasepro/types";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Auto-create auth tables if they don't exist.
|
|
9
|
+
*
|
|
10
|
+
* @param db — Drizzle database instance
|
|
11
|
+
* @param collection — The collection that represents auth users.
|
|
12
|
+
* When omitted, a default `rebase.users` table is created.
|
|
13
|
+
*/
|
|
14
|
+
export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: CollectionConfig): Promise<void> {
|
|
15
|
+
logger.info("🔍 Checking auth tables...");
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Resolve dynamic user table name and ID type from the collection
|
|
19
|
+
let usersTableName = '"rebase"."users"';
|
|
20
|
+
let userIdType = "TEXT";
|
|
21
|
+
let usersSchema = "rebase";
|
|
22
|
+
let resolvedTable = "users";
|
|
23
|
+
if (collection) {
|
|
24
|
+
resolvedTable = ("table" in collection && typeof collection.table === "string")
|
|
25
|
+
? collection.table
|
|
26
|
+
: collection.slug;
|
|
27
|
+
usersSchema = ("schema" in collection && typeof collection.schema === "string")
|
|
28
|
+
? collection.schema
|
|
29
|
+
: "public";
|
|
30
|
+
usersTableName = usersSchema === "public"
|
|
31
|
+
? `"${resolvedTable}"`
|
|
32
|
+
: `"${usersSchema}"."${resolvedTable}"`;
|
|
33
|
+
|
|
34
|
+
// Derive ID column type from collection properties
|
|
35
|
+
const idProp = collection.properties?.id;
|
|
36
|
+
if (idProp) {
|
|
37
|
+
const isId = ("isId" in idProp) ? (idProp as unknown as Record<string, unknown>).isId : undefined;
|
|
38
|
+
if (isId === "uuid") {
|
|
39
|
+
userIdType = "UUID";
|
|
40
|
+
} else if (isId === "autoincrement") {
|
|
41
|
+
userIdType = "INTEGER";
|
|
42
|
+
}
|
|
43
|
+
// Otherwise keep TEXT as default
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Introspect the database to find the actual type of usersTableName's ID column if the table exists
|
|
48
|
+
try {
|
|
49
|
+
const result = await db.execute(sql`
|
|
50
|
+
SELECT data_type
|
|
51
|
+
FROM information_schema.columns
|
|
52
|
+
WHERE table_schema = ${usersSchema}
|
|
53
|
+
AND table_name = ${resolvedTable}
|
|
54
|
+
AND column_name = 'id'
|
|
55
|
+
`);
|
|
56
|
+
if (result && result.rows && result.rows.length > 0) {
|
|
57
|
+
const dbType = String((result.rows[0] as { data_type: string }).data_type).toUpperCase();
|
|
58
|
+
if (dbType === "UUID") {
|
|
59
|
+
userIdType = "UUID";
|
|
60
|
+
} else if (dbType === "INTEGER" || dbType === "SMALLINT" || dbType === "BIGINT") {
|
|
61
|
+
userIdType = "INTEGER";
|
|
62
|
+
} else {
|
|
63
|
+
userIdType = "TEXT";
|
|
64
|
+
}
|
|
65
|
+
logger.info(`✨ Detected ${usersTableName}.id type from database: ${dbType}. Using user_id type: ${userIdType}`);
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// Ignore introspection errors, fallback to derived/default type
|
|
69
|
+
logger.warn(`⚠️ Failed to introspect ${usersTableName}.id type from database, falling back to config type: ${userIdType}`, { error: err });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
// ── Create schemas (idempotent) ──────────────────────────────────
|
|
74
|
+
if (usersSchema !== "public") {
|
|
75
|
+
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(usersSchema)}`);
|
|
76
|
+
}
|
|
77
|
+
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
|
|
78
|
+
|
|
79
|
+
const authSchema = usersSchema === "public" ? "rebase" : usersSchema;
|
|
80
|
+
const userIdentitiesTable = `"${authSchema}"."user_identities"`;
|
|
81
|
+
const refreshTokensTableName = `"${authSchema}"."refresh_tokens"`;
|
|
82
|
+
const passwordResetTokensTableName = `"${authSchema}"."password_reset_tokens"`;
|
|
83
|
+
const appConfigTableName = `"${authSchema}"."app_config"`;
|
|
84
|
+
|
|
85
|
+
// ── Create users table (idempotent) ─────────────────────────────
|
|
86
|
+
// The users table MUST be created before any dependent auth tables
|
|
87
|
+
// (user_identities, refresh_tokens, etc.) because they all hold
|
|
88
|
+
// foreign keys referencing users(id). When a developer runs
|
|
89
|
+
// `pnpm dev` for the first time without `db:migrate`, this ensures
|
|
90
|
+
// the server can self-bootstrap.
|
|
91
|
+
const idDefault = userIdType === "UUID"
|
|
92
|
+
? "DEFAULT gen_random_uuid()"
|
|
93
|
+
: userIdType === "INTEGER"
|
|
94
|
+
? "GENERATED ALWAYS AS IDENTITY"
|
|
95
|
+
: "DEFAULT gen_random_uuid()::text";
|
|
96
|
+
|
|
97
|
+
await db.execute(sql`
|
|
98
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(usersTableName)} (
|
|
99
|
+
id ${sql.raw(userIdType)} PRIMARY KEY ${sql.raw(idDefault)},
|
|
100
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
101
|
+
display_name VARCHAR(255),
|
|
102
|
+
photo_url VARCHAR(500),
|
|
103
|
+
roles TEXT[] DEFAULT '{}' NOT NULL,
|
|
104
|
+
password_hash VARCHAR(255),
|
|
105
|
+
email_verified BOOLEAN DEFAULT FALSE NOT NULL,
|
|
106
|
+
email_verification_token VARCHAR(255),
|
|
107
|
+
email_verification_sent_at TIMESTAMP WITH TIME ZONE,
|
|
108
|
+
is_anonymous BOOLEAN DEFAULT FALSE NOT NULL,
|
|
109
|
+
metadata JSONB DEFAULT '{}' NOT NULL,
|
|
110
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
111
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
|
|
112
|
+
)
|
|
113
|
+
`);
|
|
114
|
+
|
|
115
|
+
// ── Create dependent auth tables (idempotent) ───────────────────
|
|
116
|
+
|
|
117
|
+
// Create user_identities table
|
|
118
|
+
await db.execute(sql`
|
|
119
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(userIdentitiesTable)} (
|
|
120
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
121
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
122
|
+
provider TEXT NOT NULL,
|
|
123
|
+
provider_id TEXT NOT NULL,
|
|
124
|
+
profile_data JSONB,
|
|
125
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
126
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
127
|
+
UNIQUE(provider, provider_id)
|
|
128
|
+
)
|
|
129
|
+
`);
|
|
130
|
+
|
|
131
|
+
// Create indexes on user_identities
|
|
132
|
+
await db.execute(sql`
|
|
133
|
+
CREATE INDEX IF NOT EXISTS idx_user_identities_user
|
|
134
|
+
ON ${sql.raw(userIdentitiesTable)}(user_id)
|
|
135
|
+
`);
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
// Create refresh tokens table (includes user_agent, ip_address, and unique constraint)
|
|
139
|
+
await db.execute(sql`
|
|
140
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(refreshTokensTableName)} (
|
|
141
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
142
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
143
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
144
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
145
|
+
user_agent TEXT,
|
|
146
|
+
ip_address TEXT,
|
|
147
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
148
|
+
CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
|
|
149
|
+
)
|
|
150
|
+
`);
|
|
151
|
+
|
|
152
|
+
// Create index on token_hash for faster lookups
|
|
153
|
+
await db.execute(sql`
|
|
154
|
+
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash
|
|
155
|
+
ON ${sql.raw(refreshTokensTableName)}(token_hash)
|
|
156
|
+
`);
|
|
157
|
+
|
|
158
|
+
// Create index on user_id for cleanup operations
|
|
159
|
+
await db.execute(sql`
|
|
160
|
+
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user
|
|
161
|
+
ON ${sql.raw(refreshTokensTableName)}(user_id)
|
|
162
|
+
`);
|
|
163
|
+
|
|
164
|
+
// Create password reset tokens table
|
|
165
|
+
await db.execute(sql`
|
|
166
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(passwordResetTokensTableName)} (
|
|
167
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
168
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
169
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
170
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
171
|
+
used_at TIMESTAMP WITH TIME ZONE,
|
|
172
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
173
|
+
)
|
|
174
|
+
`);
|
|
175
|
+
|
|
176
|
+
// Create index on token_hash for password reset lookups
|
|
177
|
+
await db.execute(sql`
|
|
178
|
+
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_hash
|
|
179
|
+
ON ${sql.raw(passwordResetTokensTableName)}(token_hash)
|
|
180
|
+
`);
|
|
181
|
+
|
|
182
|
+
// Create index on user_id for password reset cleanup
|
|
183
|
+
await db.execute(sql`
|
|
184
|
+
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user
|
|
185
|
+
ON ${sql.raw(passwordResetTokensTableName)}(user_id)
|
|
186
|
+
`);
|
|
187
|
+
|
|
188
|
+
// Create magic link tokens table
|
|
189
|
+
const magicLinkTokensTableName = `"${authSchema}"."magic_link_tokens"`;
|
|
190
|
+
await db.execute(sql`
|
|
191
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(magicLinkTokensTableName)} (
|
|
192
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
193
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
194
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
195
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
196
|
+
used_at TIMESTAMP WITH TIME ZONE,
|
|
197
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
198
|
+
)
|
|
199
|
+
`);
|
|
200
|
+
|
|
201
|
+
// Create index on token_hash for magic link lookups
|
|
202
|
+
await db.execute(sql`
|
|
203
|
+
CREATE INDEX IF NOT EXISTS idx_magic_link_tokens_hash
|
|
204
|
+
ON ${sql.raw(magicLinkTokensTableName)}(token_hash)
|
|
205
|
+
`);
|
|
206
|
+
|
|
207
|
+
// Create index on user_id for magic link cleanup
|
|
208
|
+
await db.execute(sql`
|
|
209
|
+
CREATE INDEX IF NOT EXISTS idx_magic_link_tokens_user
|
|
210
|
+
ON ${sql.raw(magicLinkTokensTableName)}(user_id)
|
|
211
|
+
`);
|
|
212
|
+
|
|
213
|
+
// Create app config table
|
|
214
|
+
await db.execute(sql`
|
|
215
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(appConfigTableName)} (
|
|
216
|
+
key TEXT PRIMARY KEY,
|
|
217
|
+
value JSONB NOT NULL,
|
|
218
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
219
|
+
)
|
|
220
|
+
`);
|
|
221
|
+
|
|
222
|
+
// Create the `auth` schema with PostgreSQL RLS helper functions.
|
|
223
|
+
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS auth`);
|
|
224
|
+
|
|
225
|
+
// Use an advisory transaction lock to serialize function recreation during HMR
|
|
226
|
+
await db.transaction(async (tx) => {
|
|
227
|
+
await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
|
|
228
|
+
|
|
229
|
+
await tx.execute(sql`
|
|
230
|
+
CREATE OR REPLACE FUNCTION auth.uid() RETURNS text AS $$
|
|
231
|
+
SELECT NULLIF(current_setting('app.user_id', true), '');
|
|
232
|
+
$$ LANGUAGE sql STABLE
|
|
233
|
+
`);
|
|
234
|
+
|
|
235
|
+
await tx.execute(sql`
|
|
236
|
+
CREATE OR REPLACE FUNCTION auth.jwt() RETURNS jsonb AS $$
|
|
237
|
+
SELECT COALESCE(
|
|
238
|
+
NULLIF(current_setting('app.jwt', true), ''),
|
|
239
|
+
'{}'
|
|
240
|
+
)::jsonb;
|
|
241
|
+
$$ LANGUAGE sql STABLE
|
|
242
|
+
`);
|
|
243
|
+
|
|
244
|
+
await tx.execute(sql`
|
|
245
|
+
CREATE OR REPLACE FUNCTION auth.roles() RETURNS text AS $$
|
|
246
|
+
SELECT COALESCE(NULLIF(current_setting('app.user_roles', true), ''), '');
|
|
247
|
+
$$ LANGUAGE sql STABLE
|
|
248
|
+
`);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Seed default roles if none exist
|
|
252
|
+
// (no-op: roles are now stored inline on the users table)
|
|
253
|
+
|
|
254
|
+
// ── Migration: Add is_anonymous column (safe for existing tables) ────
|
|
255
|
+
await db.execute(sql`
|
|
256
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
257
|
+
ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
|
|
258
|
+
`);
|
|
259
|
+
|
|
260
|
+
// ── Migration: Add inline roles column (safe for existing tables) ────
|
|
261
|
+
await db.execute(sql`
|
|
262
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
263
|
+
ADD COLUMN IF NOT EXISTS roles TEXT[] DEFAULT '{}' NOT NULL
|
|
264
|
+
`);
|
|
265
|
+
|
|
266
|
+
// ── Migration: Copy roles from legacy junction table to inline column ──
|
|
267
|
+
// If the old rebase.user_roles and rebase.roles tables exist, migrate
|
|
268
|
+
// the data into the new TEXT[] column then drop the legacy tables.
|
|
269
|
+
try {
|
|
270
|
+
const legacyCheck = await db.execute(sql`
|
|
271
|
+
SELECT EXISTS (
|
|
272
|
+
SELECT 1 FROM information_schema.tables
|
|
273
|
+
WHERE table_schema = 'rebase' AND table_name = 'user_roles'
|
|
274
|
+
) AS has_user_roles
|
|
275
|
+
`);
|
|
276
|
+
const hasLegacyTables = (legacyCheck.rows[0] as { has_user_roles: boolean }).has_user_roles;
|
|
277
|
+
|
|
278
|
+
if (hasLegacyTables) {
|
|
279
|
+
logger.info("🔄 Migrating roles from legacy user_roles table...");
|
|
280
|
+
// Update users' roles column from the junction table
|
|
281
|
+
await db.execute(sql`
|
|
282
|
+
UPDATE ${sql.raw(usersTableName)} u
|
|
283
|
+
SET roles = COALESCE((
|
|
284
|
+
SELECT array_agg(ur.role_id)
|
|
285
|
+
FROM "rebase"."user_roles" ur
|
|
286
|
+
WHERE ur.user_id = u.id
|
|
287
|
+
), '{}')
|
|
288
|
+
WHERE u.roles = '{}' OR u.roles IS NULL
|
|
289
|
+
`);
|
|
290
|
+
|
|
291
|
+
// Drop legacy tables (junction first due to FK)
|
|
292
|
+
await db.execute(sql`DROP TABLE IF EXISTS "rebase"."user_roles" CASCADE`);
|
|
293
|
+
await db.execute(sql`DROP TABLE IF EXISTS "rebase"."roles" CASCADE`);
|
|
294
|
+
logger.info("✅ Legacy roles tables migrated and dropped");
|
|
295
|
+
}
|
|
296
|
+
} catch (migrationError: unknown) {
|
|
297
|
+
// Non-fatal: log and continue — the column exists and will work
|
|
298
|
+
logger.warn(`⚠️ Legacy roles migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── MFA tables ──────────────────────────────────────────────────────
|
|
302
|
+
const mfaFactorsTableName = `"${authSchema}"."mfa_factors"`;
|
|
303
|
+
const mfaChallengesTableName = `"${authSchema}"."mfa_challenges"`;
|
|
304
|
+
const recoveryCodesTableName = `"${authSchema}"."recovery_codes"`;
|
|
305
|
+
|
|
306
|
+
// Create mfa_factors table
|
|
307
|
+
await db.execute(sql`
|
|
308
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(mfaFactorsTableName)} (
|
|
309
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
310
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
311
|
+
factor_type TEXT NOT NULL DEFAULT 'totp',
|
|
312
|
+
secret_encrypted TEXT NOT NULL,
|
|
313
|
+
friendly_name TEXT,
|
|
314
|
+
verified BOOLEAN DEFAULT FALSE,
|
|
315
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
316
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
317
|
+
)
|
|
318
|
+
`);
|
|
319
|
+
|
|
320
|
+
// Create indexes on mfa_factors
|
|
321
|
+
await db.execute(sql`
|
|
322
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
|
|
323
|
+
ON ${sql.raw(mfaFactorsTableName)}(user_id)
|
|
324
|
+
`);
|
|
325
|
+
|
|
326
|
+
// Create mfa_challenges table
|
|
327
|
+
await db.execute(sql`
|
|
328
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(mfaChallengesTableName)} (
|
|
329
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
330
|
+
factor_id TEXT NOT NULL REFERENCES ${sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
|
|
331
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
332
|
+
verified_at TIMESTAMP WITH TIME ZONE,
|
|
333
|
+
ip_address TEXT,
|
|
334
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
|
|
335
|
+
)
|
|
336
|
+
`);
|
|
337
|
+
|
|
338
|
+
// Create indexes on mfa_challenges
|
|
339
|
+
await db.execute(sql`
|
|
340
|
+
CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
|
|
341
|
+
ON ${sql.raw(mfaChallengesTableName)}(factor_id)
|
|
342
|
+
`);
|
|
343
|
+
|
|
344
|
+
// Create recovery_codes table
|
|
345
|
+
await db.execute(sql`
|
|
346
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(recoveryCodesTableName)} (
|
|
347
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
348
|
+
user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
349
|
+
code_hash TEXT NOT NULL,
|
|
350
|
+
used_at TIMESTAMP WITH TIME ZONE,
|
|
351
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
352
|
+
)
|
|
353
|
+
`);
|
|
354
|
+
|
|
355
|
+
// Create indexes on recovery_codes
|
|
356
|
+
await db.execute(sql`
|
|
357
|
+
CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
|
|
358
|
+
ON ${sql.raw(recoveryCodesTableName)}(user_id)
|
|
359
|
+
`);
|
|
360
|
+
|
|
361
|
+
logger.info("✅ Auth tables ready");
|
|
362
|
+
} catch (error) {
|
|
363
|
+
logger.error("❌ Failed to create auth tables", { error });
|
|
364
|
+
logger.warn("⚠️ Continuing without creating auth tables.");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|