Strip surrounding quotes from script args; add diag-user

When npm run create-user is invoked from Windows Git Bash with
single-quoted values (--password 'foo' --name 'Berwn'), the quotes
survive into process.argv and end up stored in the DB. Login fails
silently because the stored hash is for 'foo' but the user types foo.

create-user and diag-user now strip a single set of matching surrounding
quotes from every --flag value. Real values that need literal leading
and trailing quotes can be escaped.

diag-user prints the full user row (email, normalized email, hash
prefix, isActive, memberships, session count) and optionally verifies a
password. Useful whenever a login mystery shows up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 16:39:16 +07:00
parent 98fe341e80
commit b7807e41e0
2 changed files with 109 additions and 1 deletions
+11 -1
View File
@@ -6,9 +6,19 @@ import { users, companies, companyUsers } from '../src/lib/server/db/schema/tena
import { hashPassword } from '../src/lib/server/auth/password'; import { hashPassword } from '../src/lib/server/auth/password';
import { normalizeEmail } from '../src/lib/utils/email'; import { normalizeEmail } from '../src/lib/utils/email';
function stripSurroundingQuotes(s: string | undefined): string | undefined {
if (!s || s.length < 2) return s;
const first = s[0];
const last = s[s.length - 1];
if ((first === "'" && last === "'") || (first === '"' && last === '"')) {
return s.slice(1, -1);
}
return s;
}
function readArg(flag: string, fallback?: string): string | undefined { function readArg(flag: string, fallback?: string): string | undefined {
const i = process.argv.indexOf(flag); const i = process.argv.indexOf(flag);
return i >= 0 ? process.argv[i + 1] : fallback; return stripSurroundingQuotes(i >= 0 ? process.argv[i + 1] : fallback);
} }
async function main() { async function main() {
+98
View File
@@ -0,0 +1,98 @@
import 'dotenv/config';
import { eq } from 'drizzle-orm';
import { db, pool } from '../src/lib/server/db/client';
import { users, companyUsers, companies, sessions } from '../src/lib/server/db/schema/tenancy';
import { verifyPassword } from '../src/lib/server/auth/password';
import { normalizeEmail } from '../src/lib/utils/email';
function stripSurroundingQuotes(s: string | undefined): string | undefined {
if (!s || s.length < 2) return s;
const first = s[0];
const last = s[s.length - 1];
if ((first === "'" && last === "'") || (first === '"' && last === '"')) {
return s.slice(1, -1);
}
return s;
}
function readArg(flag: string, fallback?: string): string | undefined {
const i = process.argv.indexOf(flag);
return stripSurroundingQuotes(i >= 0 ? process.argv[i + 1] : fallback);
}
async function main() {
const emailArg = readArg('--email');
const passwordArg = readArg('--password');
if (!emailArg) {
console.error('Usage: npx tsx scripts/diag-user.ts --email <addr> [--password <pw>]');
process.exit(1);
}
const normalized = normalizeEmail(emailArg);
console.log('Looking up:');
console.log(' input email :', JSON.stringify(emailArg));
console.log(' normalized lookup :', JSON.stringify(normalized));
const [u] = await db
.select()
.from(users)
.where(eq(users.emailNormalized, normalized))
.limit(1);
if (!u) {
console.log('\n❌ NO USER FOUND at that normalized email.');
const all = await db
.select({ email: users.email, en: users.emailNormalized })
.from(users);
console.log(`\nAll ${all.length} users in DB:`);
for (const row of all) console.log(` ${row.email} (normalized=${row.en})`);
await pool.end();
return;
}
console.log('\n✅ User row:');
console.log(' id :', u.id);
console.log(' email :', u.email);
console.log(' emailNormalized :', u.emailNormalized);
console.log(' displayName :', u.displayName);
console.log(' isActive :', u.isActive);
console.log(' passwordHash :', u.passwordHash ? `${u.passwordHash.slice(0, 20)}… (len=${u.passwordHash.length})` : 'NULL');
console.log(' oidcSubject :', u.oidcSubject ?? 'null');
console.log(' deletedAt :', u.deletedAt ?? 'null');
console.log(' lastLoginAt :', u.lastLoginAt ?? 'never');
const memberships = await db
.select({ companyName: companies.name, role: companyUsers.role })
.from(companyUsers)
.innerJoin(companies, eq(companies.id, companyUsers.companyId))
.where(eq(companyUsers.userId, u.id));
console.log('\nCompany memberships:');
if (memberships.length === 0) console.log(' (none)');
for (const m of memberships) console.log(` ${m.companyName} role=${m.role}`);
const sessionCount = await db.select().from(sessions).where(eq(sessions.userId, u.id));
console.log('\nActive sessions:', sessionCount.length);
if (passwordArg) {
console.log('\nVerifying password…');
if (!u.passwordHash) {
console.log(' ❌ user has no password hash');
} else {
try {
const ok = await verifyPassword(u.passwordHash, passwordArg);
console.log(' result:', ok ? '✅ MATCH' : '❌ MISMATCH');
} catch (err) {
console.log(' ❌ verify threw:', (err as Error).message);
}
}
}
await pool.end();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});