Skip to content

Commit

Permalink
🐛 fix: rollback if email already exists (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
Aravind-Kannan authored Oct 25, 2023
1 parent d57f176 commit 1db3c37
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.hypto.iam.server.exceptions

import io.ktor.server.plugins.BadRequestException
import kotlin.reflect.KClass
import kotlin.reflect.full.primaryConstructor
import org.jooq.exception.DataAccessException

object DbExceptionHandler {
data class DbExceptionMap(
val errorMessage: String,
val constraintRegex: Regex,
val appExceptions: Map<String, Pair<KClass<out Exception>, String>>
)

private val customExceptions = setOf(
BadRequestException::class,
EntityNotFoundException::class,
EntityAlreadyExistsException::class
)
private val duplicateConstraintRegex = "\"(.*)?\"".toRegex()
private val foreignKeyConstraintRegex = "foreign key constraint \"(.*)?\"".toRegex()

private val duplicateExceptions = mapOf<String, Pair<KClass<out Exception>, String>>()

private val foreignKeyExceptions = mapOf<String, Pair<KClass<out Exception>, String>>()

private val dbExceptionMap = listOf(
DbExceptionMap(
"duplicate key value violates unique constraint",
duplicateConstraintRegex,
duplicateExceptions
),
DbExceptionMap(
"violates foreign key constraint",
foreignKeyConstraintRegex,
foreignKeyExceptions
)
)

fun mapToApplicationException(e: DataAccessException): Exception {
var finalException: Exception? = null
val causeMessage = e.cause?.message ?: e.message

e.cause?.let {
if (customExceptions.contains(it::class)) {
return it as Exception
}
}

dbExceptionMap.forEach {
if (causeMessage?.contains(it.errorMessage) == true) {
val constraintKey = it.constraintRegex.find(causeMessage)?.groups?.get(1)?.value
val exceptionPair = it.appExceptions[constraintKey]
if (exceptionPair != null) {
finalException = exceptionPair.first.primaryConstructor?.call(exceptionPair.second, e)
}
}
}

return finalException ?: InternalException("Unknown error occurred", e)
}
}
50 changes: 30 additions & 20 deletions src/main/kotlin/com/hypto/iam/server/service/PasscodeService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.hypto.iam.server.db.repositories.PasscodeRepo
import com.hypto.iam.server.db.repositories.PoliciesRepo
import com.hypto.iam.server.db.repositories.UserRepo
import com.hypto.iam.server.db.tables.records.PasscodesRecord
import com.hypto.iam.server.exceptions.DbExceptionHandler
import com.hypto.iam.server.exceptions.EntityAlreadyExistsException
import com.hypto.iam.server.exceptions.EntityNotFoundException
import com.hypto.iam.server.exceptions.PasscodeLimitExceededException
Expand All @@ -21,12 +22,14 @@ import com.hypto.iam.server.security.UserPrincipal
import com.hypto.iam.server.utils.ApplicationIdUtil
import com.hypto.iam.server.utils.EncryptUtil
import com.hypto.iam.server.validators.InviteMetadata
import com.txman.TxMan
import io.ktor.server.plugins.BadRequestException
import io.ktor.util.logging.error
import java.time.LocalDateTime
import java.util.Base64
import mu.KotlinLogging
import org.apache.http.client.utils.URIBuilder
import org.jooq.exception.DataAccessException
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import software.amazon.awssdk.services.ses.SesClient
Expand Down Expand Up @@ -62,6 +65,7 @@ class PasscodeServiceImpl : KoinComponent, PasscodeService {
private val passcodeRepo: PasscodeRepo by inject()
private val gson: Gson by inject()
private val encryptUtil: EncryptUtil by inject()
private val txMan: TxMan by inject()

override suspend fun encryptMetadata(metadata: Map<String, Any>): String {
val metadataJson = gson.toJson(metadata)
Expand Down Expand Up @@ -102,26 +106,32 @@ class PasscodeServiceImpl : KoinComponent, PasscodeService {
}

val validUntil = LocalDateTime.now().plusSeconds(appConfig.app.passcodeValiditySeconds)

val passcodeRecord = PasscodesRecord().apply {
this.id = idGenerator.passcodeId()
this.email = email
this.organizationId = if (purpose == Purpose.signup) null else organizationId
this.validUntil = validUntil
this.purpose = purpose.toString()
this.createdAt = LocalDateTime.now()
this.metadata = metadata?.let { encryptMetadata(it) }
}
val passcode = passcodeRepo.createPasscode(passcodeRecord)
val response = when (purpose) {
Purpose.signup -> sendSignupPasscode(email, passcode.id)
Purpose.reset -> sendResetPassword(email, organizationId, passcode.id)
Purpose.invite -> sendInviteUserPasscode(
email,
organizationId!!,
passcode.id,
principal ?: throw AuthorizationException("User is not authorized")
)
val response = try {
txMan.wrap {
val passcodeRecord = PasscodesRecord().apply {
this.id = idGenerator.passcodeId()
this.email = email
this.organizationId = if (purpose == Purpose.signup) null else organizationId
this.validUntil = validUntil
this.purpose = purpose.toString()
this.createdAt = LocalDateTime.now()
this.metadata = metadata?.let { encryptMetadata(it) }
}
val passcode = passcodeRepo.createPasscode(passcodeRecord)
when (purpose) {
Purpose.signup -> sendSignupPasscode(email, passcode.id)
Purpose.reset -> sendResetPassword(email, organizationId, passcode.id)
Purpose.invite -> sendInviteUserPasscode(
email,
organizationId!!,
passcode.id,
principal ?: throw AuthorizationException("User is not authorized")
)
}
}
} catch (e: DataAccessException) {
logger.error { "Error occurred while creating passcode record: $e" }
throw DbExceptionHandler.mapToApplicationException(e)
}
return BaseSuccessResponse(response)
}
Expand Down

0 comments on commit 1db3c37

Please sign in to comment.