fix(#1395): accounts.issuer column + credential-only backfill — password auth on fresh installs (#1401)
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/publish Pipeline failed
Co-authored-by: code-infra-01 <[email protected]>
This commit was merged in pull request #1401.
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE "accounts" ADD COLUMN "issuer" text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
-- Backfill (#1395): better-auth >=1.7 sign-in filters accounts on
|
||||||
|
-- (provider_id = 'credential' AND issuer = 'local:credential'). Existing
|
||||||
|
-- credential rows predate the column and would fail that filter on upgraded
|
||||||
|
-- installs. Credential rows ONLY: better-auth owns issuer semantics for
|
||||||
|
-- oauth/sso rows going forward (each provider's real issuer value), so those
|
||||||
|
-- stay NULL until the provider's next flow writes them.
|
||||||
|
UPDATE "accounts" SET "issuer" = 'local:credential' WHERE "provider_id" = 'credential' AND "issuer" IS NULL;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -120,6 +120,13 @@
|
|||||||
"when": 1784050648841,
|
"when": 1784050648841,
|
||||||
"tag": "0016_salty_morlocks",
|
"tag": "0016_salty_morlocks",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 17,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1787609223282,
|
||||||
|
"tag": "0017_accounts_issuer",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -51,6 +51,75 @@ describe('runPgliteMigrations', () => {
|
|||||||
await expect(runPgliteMigrations(handle)).resolves.toBeUndefined();
|
await expect(runPgliteMigrations(handle)).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('gives accounts an issuer column (#1395) — better-auth >=1.7 requires it', async () => {
|
||||||
|
await runPgliteMigrations(handle);
|
||||||
|
|
||||||
|
const result = (await handle.db.execute(sql`
|
||||||
|
SELECT column_name, is_nullable, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'accounts' AND column_name = 'issuer'
|
||||||
|
`)) as unknown as {
|
||||||
|
rows: Array<{ column_name: string; is_nullable: string; data_type: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Nullable by design: the 1.5.x line this repo's lockfile resolves to does
|
||||||
|
// not write the field; 1.7+ populates it. One schema serves both.
|
||||||
|
expect(result.rows).toHaveLength(1);
|
||||||
|
expect(result.rows[0]?.is_nullable).toBe('YES');
|
||||||
|
expect(result.rows[0]?.data_type).toBe('text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('backfills ONLY credential rows with the synthetic issuer (#1395 upgrade path)', async () => {
|
||||||
|
// Simulate an upgraded install: migrate through 0016 only, seed pre-issuer
|
||||||
|
// rows (one credential, one oauth), then apply 0017 and discriminate.
|
||||||
|
const client = (handle.db as unknown as { $client: PgliteExec }).$client;
|
||||||
|
|
||||||
|
// Migrate to 0016 by replaying every ledger file except 0017 — the ledger
|
||||||
|
// table gates re-application, so a plain replay of 0000..0016 is enough.
|
||||||
|
const fs = await import('node:fs');
|
||||||
|
const path = await import('node:path');
|
||||||
|
const dir = path.join(import.meta.dirname, '..', 'drizzle');
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(dir)
|
||||||
|
.filter((f) => /^\d{4}_.*\.sql$/.test(f) && f < '0017')
|
||||||
|
.sort();
|
||||||
|
for (const f of files) {
|
||||||
|
const raw = fs.readFileSync(path.join(dir, f), 'utf-8');
|
||||||
|
for (const stmt of raw.split('--> statement-breakpoint')) {
|
||||||
|
const trimmed = stmt.trim();
|
||||||
|
if (trimmed) await client.exec(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.exec(`
|
||||||
|
INSERT INTO users (id, name, email, email_verified, created_at, updated_at)
|
||||||
|
VALUES ('u1', 'Legacy User', '[email protected]', true, now(), now());
|
||||||
|
INSERT INTO accounts (id, account_id, provider_id, user_id, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
('a1', '[email protected]', 'credential', 'u1', now(), now()),
|
||||||
|
('a2', 'oauth-provider-1', 'google', 'u1', now(), now());
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Apply 0017 (column + backfill).
|
||||||
|
const sql0017 = fs.readFileSync(path.join(dir, '0017_accounts_issuer.sql'), 'utf-8');
|
||||||
|
for (const stmt of sql0017.split('--> statement-breakpoint')) {
|
||||||
|
const trimmed = stmt.trim();
|
||||||
|
if (trimmed) await client.exec(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = (await handle.db.execute(sql`
|
||||||
|
SELECT provider_id, issuer FROM accounts ORDER BY id
|
||||||
|
`)) as unknown as { rows: Array<{ provider_id: string; issuer: string | null }> };
|
||||||
|
|
||||||
|
const byProvider = new Map(rows.rows.map((r) => [r.provider_id, r.issuer]));
|
||||||
|
// Credential rows get better-auth's synthetic local issuer — the value
|
||||||
|
// sign-in filters on (better-auth dist createLocalAccountIssuer).
|
||||||
|
expect(byProvider.get('credential')).toBe('local:credential');
|
||||||
|
// OAuth rows are LEFT NULL: better-auth owns their issuer semantics going
|
||||||
|
// forward (each provider's real issuer on its next flow).
|
||||||
|
expect(byProvider.get('google')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('surfaces statement-level error context on failure and leaves no ledger row', async () => {
|
it('surfaces statement-level error context on failure and leaves no ledger row', async () => {
|
||||||
// Pre-create a `users` table that conflicts with migration 0000's CREATE TABLE,
|
// Pre-create a `users` table that conflicts with migration 0000's CREATE TABLE,
|
||||||
// forcing it to fail without IF NOT EXISTS.
|
// forcing it to fail without IF NOT EXISTS.
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ export const accounts = pgTable(
|
|||||||
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(),
|
||||||
|
// better-auth >=1.7 requires an issuer on every account row: credential
|
||||||
|
// sign-up writes the synthetic 'local:credential', OAuth rows carry the
|
||||||
|
// provider's real issuer, and sign-in filters on (providerId, issuer).
|
||||||
|
// Nullable because the 1.5.x line this repo's lockfile resolves to does
|
||||||
|
// not know the field — 1.5 ignores it, 1.7 populates it (#1395).
|
||||||
|
issuer: text('issuer'),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id, { onDelete: 'cascade' }),
|
.references(() => users.id, { onDelete: 'cascade' }),
|
||||||
|
|||||||
Reference in New Issue
Block a user