feat: password reset flow with welcome emails
- POST /auth/forgot-password with welcome/reset email templates - POST /auth/reset-password with Zod validation, 4-hour tokens - Per-email rate limiting (3/hr) via Valkey, no user enumeration - Login page "Forgot password?" toggle with inline form - /reset-password page for setting new password from email link - Initial user seed sends welcome email instead of requiring password - CLI script for force-resetting passwords via kubectl exec - APP_URL env var in chart, removed INITIAL_USER_PASSWORD Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
51
packages/backend/src/scripts/reset-password.ts
Normal file
51
packages/backend/src/scripts/reset-password.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Force-reset a user's password from the command line.
|
||||
*
|
||||
* Usage:
|
||||
* bun run packages/backend/src/scripts/reset-password.ts <email> <new-password>
|
||||
*
|
||||
* From a customer pod:
|
||||
* kubectl exec -n customer-tvs deploy/customer-tvs-backend -- \
|
||||
* bun run src/scripts/reset-password.ts user@example.com NewPassword123!
|
||||
*/
|
||||
import postgres from 'postgres'
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { users } from '../db/schema/users.js'
|
||||
|
||||
const [email, newPassword] = process.argv.slice(2)
|
||||
|
||||
if (!email || !newPassword) {
|
||||
console.error('Usage: bun run reset-password.ts <email> <new-password>')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (newPassword.length < 12) {
|
||||
console.error('Error: Password must be at least 12 characters')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL
|
||||
if (!databaseUrl) {
|
||||
console.error('Error: DATABASE_URL is not set')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const sql = postgres(databaseUrl)
|
||||
const db = drizzle(sql)
|
||||
|
||||
const [user] = await db.select({ id: users.id, email: users.email }).from(users).where(eq(users.email, email)).limit(1)
|
||||
|
||||
if (!user) {
|
||||
console.error(`Error: No user found with email "${email}"`)
|
||||
await sql.end()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const hash = await bcrypt.hash(newPassword, 10)
|
||||
await db.update(users).set({ passwordHash: hash, updatedAt: new Date() }).where(eq(users.id, user.id))
|
||||
|
||||
console.log(`Password reset for ${email} (user ${user.id})`)
|
||||
await sql.end()
|
||||
Reference in New Issue
Block a user