feat(auth): implement Android refresh token handling and session management
- Added support for generating Android access tokens and managing refresh sessions in the auth endpoints. - Implemented new tests for login, logout, and refresh functionalities specific to Android clients. - Enhanced password reset logging with normalization and masking of email addresses. - Created a new diagnostics endpoint for password reset attempts, including filtering and summarizing logs. - Introduced a new utility for managing password reset logs with retention policies. - Added tests for password reset log utilities to ensure proper functionality and privacy compliance. - Updated WebAuthn configuration tests to validate origin handling for production and allowed origins.
This commit is contained in:
94
server/api/cms/password-reset-diagnostics.get.js
Normal file
94
server/api/cms/password-reset-diagnostics.get.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import { getUserFromToken, hasRole, readUsers } from '../../utils/auth.js'
|
||||
import {
|
||||
fingerprintResetEmail,
|
||||
normalizeResetEmail,
|
||||
PASSWORD_RESET_LOG_RETENTION_HOURS,
|
||||
readPasswordResetLogs
|
||||
} from '../../utils/password-reset-log.js'
|
||||
|
||||
function summarizeAttempts(entries) {
|
||||
const attemptsById = new Map()
|
||||
|
||||
for (const entry of [...entries].reverse()) {
|
||||
const attempt = attemptsById.get(entry.requestId) || {
|
||||
requestId: entry.requestId,
|
||||
startedAt: entry.ts,
|
||||
emailMasked: entry.emailMasked,
|
||||
ip: entry.ip,
|
||||
userId: entry.userId || null,
|
||||
steps: [],
|
||||
failed: false
|
||||
}
|
||||
|
||||
attempt.startedAt = attempt.startedAt || entry.ts
|
||||
attempt.userId = attempt.userId || entry.userId || null
|
||||
attempt.steps.push({
|
||||
ts: entry.ts,
|
||||
step: entry.step,
|
||||
status: entry.status,
|
||||
reason: entry.reason || null,
|
||||
errorCode: entry.errorCode || entry.error || null,
|
||||
errorMessage: entry.errorMessage || null
|
||||
})
|
||||
|
||||
if (
|
||||
entry.status === 'failed' ||
|
||||
entry.status === 'not_found' ||
|
||||
entry.status === 'no_account' ||
|
||||
entry.reason === 'smtp_credentials_missing'
|
||||
) {
|
||||
attempt.failed = true
|
||||
}
|
||||
|
||||
attemptsById.set(entry.requestId, attempt)
|
||||
}
|
||||
|
||||
return [...attemptsById.values()]
|
||||
.sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt)))
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const token = getCookie(event, 'auth_token')
|
||||
const currentUser = token ? await getUserFromToken(token) : null
|
||||
|
||||
if (!currentUser || !hasRole(currentUser, 'admin')) {
|
||||
throw createError({ statusCode: 403, message: 'Zugriff verweigert' })
|
||||
}
|
||||
|
||||
const query = getQuery(event)
|
||||
const email = normalizeResetEmail(query.email)
|
||||
const failedOnly = query.failedOnly !== 'false'
|
||||
const users = await readUsers()
|
||||
const logs = await readPasswordResetLogs()
|
||||
const filteredLogs = email
|
||||
? logs.filter(entry => entry.emailFingerprint === fingerprintResetEmail(email))
|
||||
: logs
|
||||
const attempts = summarizeAttempts(filteredLogs)
|
||||
.filter(attempt => !failedOnly || attempt.failed)
|
||||
|
||||
let matchingUsers = []
|
||||
if (email) {
|
||||
const term = email.toLowerCase()
|
||||
matchingUsers = users
|
||||
.filter(user => {
|
||||
const userEmail = normalizeResetEmail(user.email)
|
||||
const name = String(user.name || '').toLowerCase()
|
||||
return userEmail.includes(term) || name.includes(term)
|
||||
})
|
||||
.slice(0, 20)
|
||||
.map(user => ({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
active: user.active !== false,
|
||||
lastLogin: user.lastLogin || null
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
retentionHours: PASSWORD_RESET_LOG_RETENTION_HOURS,
|
||||
searchedEmail: email,
|
||||
matchingUsers,
|
||||
attempts
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user