mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-12 04:15:39 +08:00
Add password reset flow
This commit is contained in:
55
server/api/service/auth/forgot-password.post.ts
Normal file
55
server/api/service/auth/forgot-password.post.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { createError, getRequestURL, readBody } from 'h3'
|
||||
import { randomBytes, createHash } from 'node:crypto'
|
||||
import { User } from '../../../models/User'
|
||||
import { PasswordResetToken } from '../../../models/PasswordResetToken'
|
||||
import { sendMail } from '../../../utils/notifications'
|
||||
|
||||
interface ForgotPasswordBody {
|
||||
email?: string
|
||||
}
|
||||
|
||||
const RESET_TOKEN_TTL_MINUTES = 60
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<ForgotPasswordBody>(event)
|
||||
const email = body.email?.trim().toLowerCase()
|
||||
|
||||
if (!email) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Bitte eine E-Mail-Adresse angeben' })
|
||||
}
|
||||
|
||||
const user = await User.findOne({ email })
|
||||
if (user) {
|
||||
const token = randomBytes(32).toString('hex')
|
||||
const tokenHash = createHash('sha256').update(token).digest('hex')
|
||||
const expiresAt = new Date(Date.now() + RESET_TOKEN_TTL_MINUTES * 60 * 1000)
|
||||
|
||||
await PasswordResetToken.deleteMany({ user: user._id })
|
||||
await PasswordResetToken.create({ user: user._id, tokenHash, expiresAt })
|
||||
|
||||
const url = getRequestURL(event)
|
||||
const resetLink = `${url.origin}/forgot-password?token=${encodeURIComponent(token)}`
|
||||
|
||||
const salutation = user.name ? ` ${user.name}` : ''
|
||||
const lines = [
|
||||
`Hallo${salutation},`,
|
||||
'',
|
||||
'du hast den Link zum Zurücksetzen deines OpenSquawk-Passworts angefordert.',
|
||||
`Klicke innerhalb von ${RESET_TOKEN_TTL_MINUTES} Minuten auf den folgenden Link, um ein neues Passwort festzulegen:`,
|
||||
resetLink,
|
||||
'',
|
||||
'Wenn du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.',
|
||||
'',
|
||||
'Blue skies,',
|
||||
'dein OpenSquawk-Team',
|
||||
]
|
||||
|
||||
await sendMail({
|
||||
to: user.email,
|
||||
subject: 'OpenSquawk Passwort zurücksetzen',
|
||||
text: lines.join('\n'),
|
||||
})
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
47
server/api/service/auth/reset-password.post.ts
Normal file
47
server/api/service/auth/reset-password.post.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { createError, readBody } from 'h3'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { PasswordResetToken } from '../../../models/PasswordResetToken'
|
||||
import { User } from '../../../models/User'
|
||||
import { hashPassword } from '../../../utils/auth'
|
||||
|
||||
interface ResetPasswordBody {
|
||||
token?: string
|
||||
password?: string
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<ResetPasswordBody>(event)
|
||||
const token = body.token?.trim()
|
||||
const password = body.password?.trim()
|
||||
|
||||
if (!token || !password) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Token und neues Passwort erforderlich' })
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Das Passwort muss mindestens 8 Zeichen enthalten' })
|
||||
}
|
||||
|
||||
const tokenHash = createHash('sha256').update(token).digest('hex')
|
||||
const resetToken = await PasswordResetToken.findOne({ tokenHash })
|
||||
|
||||
if (!resetToken || (resetToken.expiresAt && resetToken.expiresAt < new Date()) || resetToken.usedAt) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Dieser Link ist ungültig oder abgelaufen' })
|
||||
}
|
||||
|
||||
const user = await User.findById(resetToken.user)
|
||||
if (!user) {
|
||||
await resetToken.deleteOne().catch(() => undefined)
|
||||
throw createError({ statusCode: 400, statusMessage: 'Dieser Link ist ungültig oder abgelaufen' })
|
||||
}
|
||||
|
||||
user.passwordHash = await hashPassword(password)
|
||||
user.tokenVersion += 1
|
||||
await user.save()
|
||||
|
||||
resetToken.usedAt = new Date()
|
||||
await resetToken.save()
|
||||
await PasswordResetToken.deleteMany({ user: user._id, _id: { $ne: resetToken._id } })
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
Reference in New Issue
Block a user