Created
March 28, 2026 22:58
-
-
Save TotalLag/82917b2cb4146ab2f97080ac7867c2b5 to your computer and use it in GitHub Desktop.
Node script to reset account passwords on Paperclip
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| /** | |
| * Paperclip Password Reset Script | |
| * | |
| * Resets a user's password by email address. | |
| * Connects directly to the embedded Postgres DB. | |
| * | |
| * Usage: | |
| * bun add -d pg @noble/hashes | |
| * bun run reset-password.js <email> <new-password> | |
| * | |
| * Example: | |
| * bun run reset-password.js user@example.com MyNewPassword123 | |
| */ | |
| import { Pool } from 'pg'; | |
| import { scryptAsync } from '@noble/hashes/scrypt.js'; | |
| import { bytesToHex, randomBytes } from '@noble/hashes/utils.js'; | |
| // === Configuration === | |
| // Embedded Postgres connection - matches embedded-postgres defaults | |
| const DB_CONFIG = { | |
| host: 'localhost', | |
| port: 54329, | |
| database: 'paperclip', | |
| user: 'paperclip', | |
| password: 'paperclip', | |
| max: 1, | |
| idleTimeoutMillis: 5000, | |
| connectionTimeoutMillis: 5000, | |
| }; | |
| // Scrypt params - must match better-auth's defaults (N:16384, r:16, p:1, dkLen:64) | |
| const SCRYPT_PARAMS = { N: 16384, r: 16, p: 1, dkLen: 64 }; | |
| /** | |
| * Hash a password using scrypt with the same params as better-auth | |
| * @param {string} password | |
| * @returns {Promise<string>} salt:hash format | |
| */ | |
| async function hashPassword(password) { | |
| const salt = bytesToHex(randomBytes(16)); | |
| const key = await scryptAsync(password.normalize('NFKC'), salt, { | |
| ...SCRYPT_PARAMS, | |
| maxmem: 128 * SCRYPT_PARAMS.N * SCRYPT_PARAMS.r * 2, | |
| }); | |
| return `${salt}:${bytesToHex(key)}`; | |
| } | |
| /** | |
| * Reset password for a user by email | |
| * @param {string} email | |
| * @param {string} newPassword | |
| */ | |
| async function resetPassword(email, newPassword) { | |
| const pool = new Pool(DB_CONFIG); | |
| try { | |
| // Connect and verify | |
| const client = await pool.connect(); | |
| console.log('✓ Connected to embedded Postgres on port 54329\n'); | |
| client.release(); | |
| // Look up user by email | |
| const userResult = await pool.query( | |
| `SELECT id, name, email FROM "user" WHERE email = $1`, | |
| [email] | |
| ); | |
| if (userResult.rows.length === 0) { | |
| console.error(`✗ No user found with email: ${email}`); | |
| process.exit(1); | |
| } | |
| const user = userResult.rows[0]; | |
| console.log(`✓ Found user:`); | |
| console.log(` ID: ${user.id}`); | |
| console.log(` Name: ${user.name}`); | |
| console.log(` Email: ${user.email}\n`); | |
| // Generate new password hash | |
| const passwordHash = await hashPassword(newPassword); | |
| console.log(`✓ Generated new scrypt hash for password: ${newPassword}`); | |
| // Update the account table (snake_case column names) | |
| const updateResult = await pool.query( | |
| `UPDATE account | |
| SET password = $1, updated_at = NOW() | |
| WHERE user_id = $2 | |
| RETURNING id, user_id, provider_id`, | |
| [passwordHash, user.id] | |
| ); | |
| if (updateResult.rows.length === 0) { | |
| // Try to find what accounts exist for this user | |
| const accountCheck = await pool.query( | |
| `SELECT id, provider_id FROM account WHERE user_id = $1`, | |
| [user.id] | |
| ); | |
| if (accountCheck.rows.length === 0) { | |
| console.error(`✗ No account found for user ID: ${user.id}`); | |
| console.log('\nNote: This user may have been created via OAuth/single sign-on'); | |
| console.log('and does not have a password-based account.'); | |
| process.exit(1); | |
| } | |
| console.log(`✗ Account exists but is not a credential provider account:`); | |
| accountCheck.rows.forEach(acc => { | |
| console.log(` Account ID: ${acc.id}, Provider: ${acc.provider_id}`); | |
| }); | |
| process.exit(1); | |
| } | |
| const updatedAccount = updateResult.rows[0]; | |
| console.log(`\n✓ Password updated successfully!`); | |
| console.log(` Account ID: ${updatedAccount.id}`); | |
| console.log(` Provider: ${updatedAccount.provider_id}`); | |
| console.log(` Updated at: ${new Date().toISOString()}`); | |
| console.log(`\n========================================`); | |
| console.log(`Password reset complete!`); | |
| console.log(`User can now log in with:`); | |
| console.log(` Email: ${email}`); | |
| console.log(` Password: ${newPassword}`); | |
| console.log(`========================================`); | |
| } catch (error) { | |
| console.error(`✗ Error: ${error.message}`); | |
| if (error.code === 'ECONNREFUSED') { | |
| console.error(`\nIs the embedded Postgres running?`); | |
| console.error(`Check if process is listening on port 54329:`); | |
| console.error(` lsof -i :54329`); | |
| } else if (error.code === '28P01') { | |
| console.error(`\nAuthentication failed. Check Postgres credentials.`); | |
| } | |
| process.exit(1); | |
| } finally { | |
| await pool.end(); | |
| } | |
| } | |
| // === CLI Entry Point === | |
| const args = process.argv.slice(2); | |
| if (args.length < 2) { | |
| console.log(` | |
| ╔══════════════════════════════════════════════════════════════╗ | |
| ║ Paperclip Password Reset Script ║ | |
| ╚══════════════════════════════════════════════════════════════╝ | |
| Usage: | |
| bun run reset-password.js <email> <new-password> | |
| Example: | |
| bun run reset-password.js user@example.com MyNewPassword123 | |
| Prerequisites: | |
| bun add -d pg @noble/hashes | |
| Notes: | |
| - Connects to embedded Postgres on port 54329 | |
| - Uses scrypt hashing (same as better-auth) | |
| - Updates the account table for the user | |
| `); | |
| process.exit(1); | |
| } | |
| const [email, password] = args; | |
| if (!email.includes('@')) { | |
| console.error(`✗ Invalid email address: ${email}`); | |
| process.exit(1); | |
| } | |
| if (password.length < 8) { | |
| console.warn(`⚠ Warning: Password is less than 8 characters`); | |
| } | |
| resetPassword(email, password).catch(err => { | |
| console.error('Fatal error:', err); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment