Self-service password reset

This commit is contained in:
2025-10-06 18:26:01 -04:00
parent 53c9ba8fd7
commit c127ea35f6
13 changed files with 683 additions and 21 deletions

View File

@@ -21,9 +21,20 @@ export interface User {
id?: number
username: string
password: string
email?: string
first_name?: string
last_name?: string
is_admin: number
}
export interface PasswordResetCode {
id?: number
email: string
code: string
expires_at: string
created_at?: string
}
export interface SermonNote {
id?: number
user_id: number
@@ -60,10 +71,23 @@ export function getDatabase() {
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
email TEXT,
first_name TEXT,
last_name TEXT,
is_admin INTEGER DEFAULT 0
)
`)
db.exec(`
CREATE TABLE IF NOT EXISTS password_reset_codes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
code TEXT NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
db.exec(`
CREATE TABLE IF NOT EXISTS sermon_notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -143,16 +167,48 @@ export function getUserByUsername(username: string) {
return db.prepare('SELECT * FROM users WHERE username = ?').get(username) as User | undefined
}
export function createUser(username: string, password: string) {
export function createUser(username: string, password: string, email?: string, firstName?: string, lastName?: string) {
const db = getDatabase()
const saltRounds = 10
const hashedPassword = bcrypt.hashSync(password, saltRounds)
return db.prepare('INSERT INTO users (username, password, is_admin) VALUES (?, ?, 0)').run(username, hashedPassword)
return db.prepare('INSERT INTO users (username, password, email, first_name, last_name, is_admin) VALUES (?, ?, ?, ?, ?, 0)')
.run(username, hashedPassword, email || null, firstName || null, lastName || null)
}
export function getUserByEmail(email: string) {
const db = getDatabase()
return db.prepare('SELECT * FROM users WHERE email = ?').get(email) as User | undefined
}
export function createPasswordResetCode(email: string, code: string, expiresAt: string) {
const db = getDatabase()
// Delete any existing codes for this email
db.prepare('DELETE FROM password_reset_codes WHERE email = ?').run(email)
return db.prepare('INSERT INTO password_reset_codes (email, code, expires_at) VALUES (?, ?, ?)')
.run(email, code, expiresAt)
}
export function getPasswordResetCode(email: string, code: string) {
const db = getDatabase()
return db.prepare('SELECT * FROM password_reset_codes WHERE email = ? AND code = ? AND expires_at > datetime("now")')
.get(email, code) as PasswordResetCode | undefined
}
export function deletePasswordResetCode(email: string) {
const db = getDatabase()
return db.prepare('DELETE FROM password_reset_codes WHERE email = ?').run(email)
}
export function resetPasswordByEmail(email: string, newPassword: string) {
const db = getDatabase()
const saltRounds = 10
const hashedPassword = bcrypt.hashSync(newPassword, saltRounds)
return db.prepare('UPDATE users SET password = ? WHERE email = ?').run(hashedPassword, email)
}
export function getAllUsers() {
const db = getDatabase()
return db.prepare('SELECT id, username, is_admin FROM users ORDER BY username').all() as Omit<User, 'password'>[]
return db.prepare('SELECT id, username, email, first_name, last_name, is_admin FROM users ORDER BY username').all() as Omit<User, 'password'>[]
}
export function deleteUser(id: number) {

39
server/utils/email.ts Normal file
View File

@@ -0,0 +1,39 @@
import nodemailer from 'nodemailer'
export async function sendPasswordResetEmail(email: string, code: string) {
const config = useRuntimeConfig()
const transporter = nodemailer.createTransport({
host: config.emailHost,
port: parseInt(config.emailPort),
secure: parseInt(config.emailPort) === 465,
auth: {
user: config.emailUser,
pass: config.emailPassword,
},
})
const mailOptions = {
from: config.emailFrom,
to: email,
subject: 'Password Reset Code - New Life Christian Church',
text: `Please enter this 6 digit code to reset your password for the New Life Christian Church sermon page: ${code}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #333;">Password Reset Request</h2>
<p>Please enter this 6 digit code to reset your password for the New Life Christian Church sermon page:</p>
<div style="background-color: #f4f4f4; padding: 20px; text-align: center; font-size: 32px; font-weight: bold; letter-spacing: 5px; margin: 20px 0;">
${code}
</div>
<p style="color: #666;">This code will expire in 15 minutes.</p>
<p style="color: #666;">If you did not request a password reset, please ignore this email.</p>
</div>
`,
}
await transporter.sendMail(mailOptions)
}
export function generateResetCode(): string {
return Math.floor(100000 + Math.random() * 900000).toString()
}