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:
+11
-1
@@ -6,9 +6,19 @@ import { users, companies, companyUsers } from '../src/lib/server/db/schema/tena
|
||||
import { hashPassword } 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 i >= 0 ? process.argv[i + 1] : fallback;
|
||||
return stripSurroundingQuotes(i >= 0 ? process.argv[i + 1] : fallback);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user