Phase 3.4: structured rate-limit retry-after + auth error logging
OtpError now carries an optional details object; rate-limit branches in checkRateLimits compute retry_after_seconds (cooldown delta for OTP_COOLDOWN, window-roll-out delta for OTP_RATE_LIMIT_PHONE / OTP_RATE_LIMIT_IP) so the client can disable Kirim OTP / Kirim ulang CTAs with a real countdown. All four sendAuthError helpers (client, mitra, shared, internal) now surface err.details and log unhandled (no statusCode) errors at level 50. New GET /api/shared/config/otp returns the resend cooldown so the OTP screen can gate the resend countdown without hardcoding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,10 +14,17 @@ const extractDeviceInfo = (request) => ({
|
||||
ip: request.ip || null,
|
||||
})
|
||||
|
||||
const sendAuthError = (reply, err) => reply.code(err.statusCode || 500).send({
|
||||
success: false,
|
||||
error: { code: err.code || 'INTERNAL', message: err.message },
|
||||
})
|
||||
const sendAuthError = (reply, err) => {
|
||||
if (!err.statusCode) reply.request.log.error({ err }, 'Unhandled auth error')
|
||||
return reply.code(err.statusCode || 500).send({
|
||||
success: false,
|
||||
error: {
|
||||
code: err.code || 'INTERNAL',
|
||||
message: err.message,
|
||||
...(err.details && { details: err.details }),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const cookieOpts = () => ({
|
||||
httpOnly: true,
|
||||
|
||||
@@ -13,10 +13,17 @@ const extractDeviceInfo = (request) => ({
|
||||
ip: request.ip || null,
|
||||
})
|
||||
|
||||
const sendAuthError = (reply, err) => reply.code(err.statusCode || 500).send({
|
||||
success: false,
|
||||
error: { code: err.code || 'INTERNAL', message: err.message },
|
||||
})
|
||||
const sendAuthError = (reply, err) => {
|
||||
if (!err.statusCode) reply.request.log.error({ err }, 'Unhandled auth error')
|
||||
return reply.code(err.statusCode || 500).send({
|
||||
success: false,
|
||||
error: {
|
||||
code: err.code || 'INTERNAL',
|
||||
message: err.message,
|
||||
...(err.details && { details: err.details }),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const clientAuthRoutes = async (app) => {
|
||||
// --- Phone OTP ---
|
||||
|
||||
@@ -9,10 +9,17 @@ const extractDeviceInfo = (request) => ({
|
||||
ip: request.ip || null,
|
||||
})
|
||||
|
||||
const sendAuthError = (reply, err) => reply.code(err.statusCode || 500).send({
|
||||
success: false,
|
||||
error: { code: err.code || 'INTERNAL', message: err.message },
|
||||
})
|
||||
const sendAuthError = (reply, err) => {
|
||||
if (!err.statusCode) reply.request.log.error({ err }, 'Unhandled auth error')
|
||||
return reply.code(err.statusCode || 500).send({
|
||||
success: false,
|
||||
error: {
|
||||
code: err.code || 'INTERNAL',
|
||||
message: err.message,
|
||||
...(err.details && { details: err.details }),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const mitraAuthRoutes = async (app) => {
|
||||
app.post('/otp/request', async (request, reply) => {
|
||||
|
||||
@@ -10,10 +10,17 @@ const extractDeviceInfo = (request) => ({
|
||||
ip: request.ip || null,
|
||||
})
|
||||
|
||||
const sendAuthError = (reply, err) => reply.code(err.statusCode || 500).send({
|
||||
success: false,
|
||||
error: { code: err.code || 'INTERNAL', message: err.message },
|
||||
})
|
||||
const sendAuthError = (reply, err) => {
|
||||
if (!err.statusCode) reply.request.log.error({ err }, 'Unhandled auth error')
|
||||
return reply.code(err.statusCode || 500).send({
|
||||
success: false,
|
||||
error: {
|
||||
code: err.code || 'INTERNAL',
|
||||
message: err.message,
|
||||
...(err.details && { details: err.details }),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sharedAuthRoutes = async (app) => {
|
||||
// Issue an anonymous customer session
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { authenticate } from '../../plugins/auth.js'
|
||||
import { getAnonymityConfig, getSensitivityConfig } from '../../services/config.service.js'
|
||||
import { getAnonymityConfig, getSensitivityConfig, getOtpRateLimits } from '../../services/config.service.js'
|
||||
|
||||
export const sharedConfigRoutes = async (app) => {
|
||||
app.get('/anonymity', async (request, reply) => {
|
||||
@@ -11,4 +11,12 @@ export const sharedConfigRoutes = async (app) => {
|
||||
const config = await getSensitivityConfig()
|
||||
return reply.send({ success: true, data: config })
|
||||
})
|
||||
|
||||
app.get('/otp', async (request, reply) => {
|
||||
const limits = await getOtpRateLimits()
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: { resend_cooldown_seconds: limits.resend_cooldown_seconds },
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user