admin login fixed.
Build & Push / Pipeline Tests (push) Failing after 1m34s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 56s
Test / API Unit Tests (push) Successful in 1m8s
Test / Homepage Unit Tests (push) Successful in 47s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 46s
Test / Dashboard Unit Tests (push) Failing after 43s
Test / API Integration Tests (push) Successful in 1m8s
Build & Push / Pipeline Tests (push) Failing after 1m34s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 56s
Test / API Unit Tests (push) Successful in 1m8s
Test / Homepage Unit Tests (push) Successful in 47s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 46s
Test / Dashboard Unit Tests (push) Failing after 43s
Test / API Integration Tests (push) Successful in 1m8s
This commit is contained in:
@@ -6,12 +6,17 @@ import { signActorToken } from '../../security/tokens'
|
||||
import qrcode from 'qrcode'
|
||||
import { getCarplaceHomepageContent, saveCarplaceHomepageContent } from '../../services/platformContentService'
|
||||
import { sendTransactionalEmail } from '../../services/notificationService'
|
||||
import { redis } from '../../lib/redis'
|
||||
import * as presenter from './admin.presenter'
|
||||
import * as repo from './admin.repo'
|
||||
import * as billingService from './admin.billing.service'
|
||||
|
||||
const ADMIN_RESET_TTL_MINUTES = 60
|
||||
const ADMIN_RECOVERY_CODE_COUNT = 10
|
||||
const ADMIN_EMAIL_OTP_TTL_MINUTES = 10
|
||||
const ADMIN_EMAIL_OTP_TTL_SECONDS = ADMIN_EMAIL_OTP_TTL_MINUTES * 60
|
||||
|
||||
const pendingAdminEmailOtps = new Map<string, { code: string; expiresAt: number }>()
|
||||
|
||||
|
||||
function generateRecoveryCode() {
|
||||
@@ -57,6 +62,61 @@ function signAdminToken(adminId: string, last2faAt?: number) {
|
||||
return signActorToken(adminId, 'admin', { expiresIn: '8h', last2faAt })
|
||||
}
|
||||
|
||||
function generateAdminEmailOtp() {
|
||||
return crypto.randomInt(100000, 1000000).toString()
|
||||
}
|
||||
|
||||
function adminEmailOtpKey(adminId: string) {
|
||||
return `admin:email-otp:${adminId}`
|
||||
}
|
||||
|
||||
async function sendAdminEmailOtp(admin: { id: string; email: string; firstName?: string | null }) {
|
||||
const code = generateAdminEmailOtp()
|
||||
const codeHash = hashPublicAccessToken(code)
|
||||
pendingAdminEmailOtps.set(admin.id, {
|
||||
code: codeHash,
|
||||
expiresAt: Date.now() + ADMIN_EMAIL_OTP_TTL_MINUTES * 60 * 1000,
|
||||
})
|
||||
await redis
|
||||
.set(adminEmailOtpKey(admin.id), codeHash, 'EX', ADMIN_EMAIL_OTP_TTL_SECONDS)
|
||||
.catch((err) => console.error('[AdminLoginEmailOtpRedisSet]', err?.message))
|
||||
|
||||
await sendTransactionalEmail({
|
||||
to: admin.email,
|
||||
subject: 'Your RentalDriveGo admin login code',
|
||||
html: `<p>Hi ${admin.firstName ?? 'Admin'},</p><p>Your admin login code is <strong>${code}</strong>.</p><p>It expires in ${ADMIN_EMAIL_OTP_TTL_MINUTES} minutes.</p>`,
|
||||
text: `Hi ${admin.firstName ?? 'Admin'},\n\nYour admin login code is ${code}.\n\nIt expires in ${ADMIN_EMAIL_OTP_TTL_MINUTES} minutes.`,
|
||||
}).catch((err) => console.error('[AdminLoginEmailOtp]', err?.message))
|
||||
}
|
||||
|
||||
async function consumeAdminEmailOtp(adminId: string, code: string | undefined) {
|
||||
if (!code) return false
|
||||
const codeHash = hashPublicAccessToken(code.trim())
|
||||
const key = adminEmailOtpKey(adminId)
|
||||
const persistedHash = await redis
|
||||
.get(key)
|
||||
.catch((err) => {
|
||||
console.error('[AdminLoginEmailOtpRedisGet]', err?.message)
|
||||
return null
|
||||
})
|
||||
if (persistedHash) {
|
||||
if (persistedHash !== codeHash) return false
|
||||
await redis.del(key).catch((err) => console.error('[AdminLoginEmailOtpRedisDel]', err?.message))
|
||||
pendingAdminEmailOtps.delete(adminId)
|
||||
return true
|
||||
}
|
||||
|
||||
const pending = pendingAdminEmailOtps.get(adminId)
|
||||
if (!pending) return false
|
||||
if (pending.expiresAt <= Date.now()) {
|
||||
pendingAdminEmailOtps.delete(adminId)
|
||||
return false
|
||||
}
|
||||
if (pending.code !== codeHash) return false
|
||||
pendingAdminEmailOtps.delete(adminId)
|
||||
return true
|
||||
}
|
||||
|
||||
function toAuditJson<T>(value: T) {
|
||||
return JSON.parse(JSON.stringify(value))
|
||||
}
|
||||
@@ -81,16 +141,20 @@ export async function login(email: string, password: string, totpCode?: string,
|
||||
if (!valid) return null
|
||||
|
||||
if (admin.totpEnabled) {
|
||||
if (!totpCode && !recoveryCode) return { totpRequired: true } as const
|
||||
if (!totpCode && !recoveryCode) {
|
||||
await sendAdminEmailOtp(admin)
|
||||
return { totpRequired: true } as const
|
||||
}
|
||||
|
||||
const validTotp = totpCode
|
||||
? authenticator.verify({ token: totpCode, secret: admin.totpSecret! })
|
||||
: false
|
||||
const validRecoveryCode = !validTotp && recoveryCode
|
||||
const validEmailOtp = !validTotp && await consumeAdminEmailOtp(admin.id, totpCode)
|
||||
const validRecoveryCode = !validTotp && !validEmailOtp && recoveryCode
|
||||
? await consumeAdminRecoveryCode(admin.id, recoveryCode)
|
||||
: false
|
||||
|
||||
if (!validTotp && !validRecoveryCode) {
|
||||
if (!validTotp && !validEmailOtp && !validRecoveryCode) {
|
||||
return { invalidTotp: true } as const
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user