Accounts can now be deleted
This commit is contained in:
parent
00dafe1da9
commit
662d6c706b
@ -6,10 +6,12 @@ import be.simplenotes.app.extensions.redirect
|
|||||||
import be.simplenotes.app.views.UserView
|
import be.simplenotes.app.views.UserView
|
||||||
import be.simplenotes.domain.security.JwtPayload
|
import be.simplenotes.domain.security.JwtPayload
|
||||||
import be.simplenotes.domain.usecases.UserService
|
import be.simplenotes.domain.usecases.UserService
|
||||||
import be.simplenotes.domain.usecases.login.*
|
import be.simplenotes.domain.usecases.users.delete.DeleteError
|
||||||
import be.simplenotes.domain.usecases.register.InvalidRegisterForm
|
import be.simplenotes.domain.usecases.users.delete.DeleteForm
|
||||||
import be.simplenotes.domain.usecases.register.RegisterForm
|
import be.simplenotes.domain.usecases.users.login.*
|
||||||
import be.simplenotes.domain.usecases.register.UserExists
|
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
|
||||||
|
import be.simplenotes.domain.usecases.users.register.RegisterForm
|
||||||
|
import be.simplenotes.domain.usecases.users.register.UserExists
|
||||||
import be.simplenotes.shared.config.JwtConfig
|
import be.simplenotes.shared.config.JwtConfig
|
||||||
import org.http4k.core.Method.GET
|
import org.http4k.core.Method.GET
|
||||||
import org.http4k.core.Request
|
import org.http4k.core.Request
|
||||||
@ -110,4 +112,38 @@ class UserController(
|
|||||||
|
|
||||||
fun logout(@Suppress("UNUSED_PARAMETER") request: Request) = Response.redirect("/")
|
fun logout(@Suppress("UNUSED_PARAMETER") request: Request) = Response.redirect("/")
|
||||||
.invalidateCookie("Authorization")
|
.invalidateCookie("Authorization")
|
||||||
|
|
||||||
|
private fun Request.deleteForm(jwtPayload: JwtPayload) =
|
||||||
|
DeleteForm(jwtPayload.username, form("password"), form("checked") != null)
|
||||||
|
|
||||||
|
fun settings(request: Request, jwtPayload: JwtPayload): Response {
|
||||||
|
if (request.method == GET)
|
||||||
|
return Response(OK).html(userView.settings(jwtPayload))
|
||||||
|
|
||||||
|
val deleteForm = request.deleteForm(jwtPayload)
|
||||||
|
val result = userService.delete(deleteForm)
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
{
|
||||||
|
when (it) {
|
||||||
|
DeleteError.Unregistered -> Response.redirect("/").invalidateCookie("Authorization")
|
||||||
|
DeleteError.WrongPassword -> Response(OK).html(
|
||||||
|
userView.settings(
|
||||||
|
jwtPayload,
|
||||||
|
error = "Wrong password"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
is DeleteError.InvalidForm -> Response(OK).html(
|
||||||
|
userView.settings(
|
||||||
|
jwtPayload,
|
||||||
|
validationErrors = it.validationErrors
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Response.redirect("/").invalidateCookie("Authorization")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,7 +42,8 @@ class Router(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val protectedRoutes = routes(
|
val protectedRoutes = routes(
|
||||||
"/account" bind GET to { TODO() },
|
"/settings" bind GET to { protected(it, userController::settings) },
|
||||||
|
"/settings" bind POST to { protected(it, userController::settings) },
|
||||||
"/export" bind POST to { TODO() },
|
"/export" bind POST to { TODO() },
|
||||||
"/notes" bind GET to { protected(it, noteController::list) },
|
"/notes" bind GET to { protected(it, noteController::list) },
|
||||||
"/notes/new" bind GET to { protected(it, noteController::new) },
|
"/notes/new" bind GET to { protected(it, noteController::new) },
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import be.simplenotes.app.views.components.submitButton
|
|||||||
import be.simplenotes.domain.security.JwtPayload
|
import be.simplenotes.domain.security.JwtPayload
|
||||||
import io.konform.validation.ValidationError
|
import io.konform.validation.ValidationError
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
|
import kotlinx.html.ButtonType.submit
|
||||||
|
|
||||||
class UserView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
|
class UserView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
|
||||||
fun register(
|
fun register(
|
||||||
@ -70,4 +71,74 @@ class UserView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun settings(
|
||||||
|
jwtPayload: JwtPayload,
|
||||||
|
error: String? = null,
|
||||||
|
validationErrors: List<ValidationError> = emptyList(),
|
||||||
|
) = renderPage("Settings", jwtPayload = jwtPayload) {
|
||||||
|
div("container mx-auto p-8") {
|
||||||
|
|
||||||
|
section("m-4 p-4 bg-gray-800 rounded") {
|
||||||
|
h1("text-xl") {
|
||||||
|
+"Welcome "
|
||||||
|
span("text-teal-200 font-semibold") { +jwtPayload.username }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section("m-4 p-4 bg-gray-800 rounded") {
|
||||||
|
p(classes = "mb-4") {
|
||||||
|
+"Export all my data in a zip file"
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
button(classes = "btn btn-teal block") { +"Export my data" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section(classes = "m-4 p-4 bg-gray-800 rounded") {
|
||||||
|
h2(classes = "mb-4 text-red-600 text-lg font-semibold") {
|
||||||
|
+"Delete my account"
|
||||||
|
}
|
||||||
|
|
||||||
|
error?.let { alert(Alert.Warning, error) }
|
||||||
|
|
||||||
|
details {
|
||||||
|
summary {
|
||||||
|
span(classes = "mb-4 font-semibold underline") {
|
||||||
|
+"Are you sure? "
|
||||||
|
+"You are about to delete this user, and this process is irreversible !"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form(classes = "mt-4", method = FormMethod.post) {
|
||||||
|
input(
|
||||||
|
id = "password",
|
||||||
|
placeholder = "Password",
|
||||||
|
autoComplete = "off",
|
||||||
|
type = InputType.password,
|
||||||
|
error = validationErrors.find { it.dataPath == ".password" }?.message
|
||||||
|
)
|
||||||
|
checkBoxInput(name = "checked") {
|
||||||
|
attributes["required"] = ""
|
||||||
|
+" Do you want to proceed ?"
|
||||||
|
}
|
||||||
|
button(type = submit, classes = "block mt-4 btn btn-red", name = "delete") { +"I'm sure" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SUMMARY(consumer: TagConsumer<*>) :
|
||||||
|
HTMLTag(
|
||||||
|
"summary", consumer, emptyMap(),
|
||||||
|
inlineTag = true,
|
||||||
|
emptyTag = false
|
||||||
|
),
|
||||||
|
HtmlInlineTag
|
||||||
|
|
||||||
|
fun DETAILS.summary(block: SUMMARY.() -> Unit = {}) {
|
||||||
|
SUMMARY(consumer).visit(block)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,18 +6,21 @@ import be.simplenotes.domain.security.PasswordHash
|
|||||||
import be.simplenotes.domain.security.SimpleJwt
|
import be.simplenotes.domain.security.SimpleJwt
|
||||||
import be.simplenotes.domain.usecases.NoteService
|
import be.simplenotes.domain.usecases.NoteService
|
||||||
import be.simplenotes.domain.usecases.UserService
|
import be.simplenotes.domain.usecases.UserService
|
||||||
import be.simplenotes.domain.usecases.login.LoginUseCase
|
|
||||||
import be.simplenotes.domain.usecases.login.LoginUseCaseImpl
|
|
||||||
import be.simplenotes.domain.usecases.markdown.MarkdownConverter
|
import be.simplenotes.domain.usecases.markdown.MarkdownConverter
|
||||||
import be.simplenotes.domain.usecases.markdown.MarkdownConverterImpl
|
import be.simplenotes.domain.usecases.markdown.MarkdownConverterImpl
|
||||||
import be.simplenotes.domain.usecases.register.RegisterUseCase
|
import be.simplenotes.domain.usecases.users.delete.DeleteUseCase
|
||||||
import be.simplenotes.domain.usecases.register.RegisterUseCaseImpl
|
import be.simplenotes.domain.usecases.users.delete.DeleteUseCaseImpl
|
||||||
|
import be.simplenotes.domain.usecases.users.login.LoginUseCase
|
||||||
|
import be.simplenotes.domain.usecases.users.login.LoginUseCaseImpl
|
||||||
|
import be.simplenotes.domain.usecases.users.register.RegisterUseCase
|
||||||
|
import be.simplenotes.domain.usecases.users.register.RegisterUseCaseImpl
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val domainModule = module {
|
val domainModule = module {
|
||||||
single<LoginUseCase> { LoginUseCaseImpl(get(), get(), get()) }
|
single<LoginUseCase> { LoginUseCaseImpl(get(), get(), get()) }
|
||||||
single<RegisterUseCase> { RegisterUseCaseImpl(get(), get()) }
|
single<RegisterUseCase> { RegisterUseCaseImpl(get(), get()) }
|
||||||
single { UserService(get(), get()) }
|
single<DeleteUseCase> { DeleteUseCaseImpl(get(), get()) }
|
||||||
|
single { UserService(get(), get(), get()) }
|
||||||
single<PasswordHash> { BcryptPasswordHash() }
|
single<PasswordHash> { BcryptPasswordHash() }
|
||||||
single { SimpleJwt(get()) }
|
single { SimpleJwt(get()) }
|
||||||
single { JwtPayloadExtractor(get()) }
|
single { JwtPayloadExtractor(get()) }
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
package be.simplenotes.domain.usecases
|
package be.simplenotes.domain.usecases
|
||||||
|
|
||||||
import be.simplenotes.domain.usecases.login.LoginUseCase
|
import be.simplenotes.domain.usecases.users.delete.DeleteUseCase
|
||||||
import be.simplenotes.domain.usecases.register.RegisterUseCase
|
import be.simplenotes.domain.usecases.users.login.LoginUseCase
|
||||||
|
import be.simplenotes.domain.usecases.users.register.RegisterUseCase
|
||||||
|
|
||||||
class UserService(
|
class UserService(
|
||||||
loginUseCase: LoginUseCase,
|
loginUseCase: LoginUseCase,
|
||||||
registerUseCase: RegisterUseCase
|
registerUseCase: RegisterUseCase,
|
||||||
) : LoginUseCase by loginUseCase, RegisterUseCase by registerUseCase
|
deleteUseCase: DeleteUseCase,
|
||||||
|
) : LoginUseCase by loginUseCase, RegisterUseCase by registerUseCase, DeleteUseCase by deleteUseCase
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
package be.simplenotes.domain.usecases.users.delete
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import arrow.core.extensions.fx
|
||||||
|
import arrow.core.rightIfNotNull
|
||||||
|
import be.simplenotes.domain.security.PasswordHash
|
||||||
|
import be.simplenotes.domain.usecases.repositories.UserRepository
|
||||||
|
import be.simplenotes.domain.validation.UserValidations
|
||||||
|
|
||||||
|
internal class DeleteUseCaseImpl(
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
private val passwordHash: PasswordHash,
|
||||||
|
) : DeleteUseCase {
|
||||||
|
override fun delete(form: DeleteForm) = Either.fx<DeleteError, Unit> {
|
||||||
|
val user = !UserValidations.validateDelete(form)
|
||||||
|
val persistedUser = !userRepository.find(user.username).rightIfNotNull { DeleteError.Unregistered }
|
||||||
|
!Either.cond(
|
||||||
|
passwordHash.verify(user.password, persistedUser.password),
|
||||||
|
{ Unit },
|
||||||
|
{ DeleteError.WrongPassword }
|
||||||
|
)
|
||||||
|
!Either.cond(userRepository.delete(persistedUser.id), { Unit }, { DeleteError.Unregistered })
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package be.simplenotes.domain.usecases.users.delete
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import io.konform.validation.ValidationErrors
|
||||||
|
|
||||||
|
sealed class DeleteError {
|
||||||
|
object Unregistered : DeleteError()
|
||||||
|
object WrongPassword : DeleteError()
|
||||||
|
class InvalidForm(val validationErrors: ValidationErrors) : DeleteError()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DeleteForm(val username: String?, val password: String?, val checked: Boolean)
|
||||||
|
|
||||||
|
interface DeleteUseCase {
|
||||||
|
fun delete(form: DeleteForm): Either<DeleteError, Unit>
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package be.simplenotes.domain.usecases.login
|
package be.simplenotes.domain.usecases.users.login
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import arrow.core.extensions.fx
|
import arrow.core.extensions.fx
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package be.simplenotes.domain.usecases.login
|
package be.simplenotes.domain.usecases.users.login
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import io.konform.validation.ValidationErrors
|
import io.konform.validation.ValidationErrors
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package be.simplenotes.domain.usecases.register
|
package be.simplenotes.domain.usecases.users.register
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import arrow.core.filterOrElse
|
import arrow.core.filterOrElse
|
||||||
@ -1,8 +1,8 @@
|
|||||||
package be.simplenotes.domain.usecases.register
|
package be.simplenotes.domain.usecases.users.register
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import be.simplenotes.domain.model.PersistedUser
|
import be.simplenotes.domain.model.PersistedUser
|
||||||
import be.simplenotes.domain.usecases.login.LoginForm
|
import be.simplenotes.domain.usecases.users.login.LoginForm
|
||||||
import io.konform.validation.ValidationErrors
|
import io.konform.validation.ValidationErrors
|
||||||
|
|
||||||
sealed class RegisterError
|
sealed class RegisterError
|
||||||
@ -4,10 +4,12 @@ import arrow.core.Either
|
|||||||
import arrow.core.left
|
import arrow.core.left
|
||||||
import arrow.core.right
|
import arrow.core.right
|
||||||
import be.simplenotes.domain.model.User
|
import be.simplenotes.domain.model.User
|
||||||
import be.simplenotes.domain.usecases.login.InvalidLoginForm
|
import be.simplenotes.domain.usecases.users.delete.DeleteError
|
||||||
import be.simplenotes.domain.usecases.login.LoginForm
|
import be.simplenotes.domain.usecases.users.delete.DeleteForm
|
||||||
import be.simplenotes.domain.usecases.register.InvalidRegisterForm
|
import be.simplenotes.domain.usecases.users.login.InvalidLoginForm
|
||||||
import be.simplenotes.domain.usecases.register.RegisterForm
|
import be.simplenotes.domain.usecases.users.login.LoginForm
|
||||||
|
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
|
||||||
|
import be.simplenotes.domain.usecases.users.register.RegisterForm
|
||||||
import io.konform.validation.Validation
|
import io.konform.validation.Validation
|
||||||
import io.konform.validation.jsonschema.maxLength
|
import io.konform.validation.jsonschema.maxLength
|
||||||
import io.konform.validation.jsonschema.minLength
|
import io.konform.validation.jsonschema.minLength
|
||||||
@ -35,4 +37,24 @@ internal object UserValidations {
|
|||||||
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
|
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
|
||||||
else return InvalidRegisterForm(errors).left()
|
else return InvalidRegisterForm(errors).left()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val deleteValidator = Validation<DeleteForm> {
|
||||||
|
DeleteForm::username required {
|
||||||
|
minLength(3)
|
||||||
|
maxLength(50)
|
||||||
|
}
|
||||||
|
DeleteForm::password required {
|
||||||
|
minLength(8)
|
||||||
|
maxLength(72)
|
||||||
|
}
|
||||||
|
DeleteForm::checked required {
|
||||||
|
addConstraint("Should be checked") { it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateDelete(form: DeleteForm): Either<DeleteError.InvalidForm, User> {
|
||||||
|
val errors = deleteValidator.validate(form).errors
|
||||||
|
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
|
||||||
|
else return DeleteError.InvalidForm(errors).left()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package be.simplenotes.domain.security
|
package be.simplenotes.domain.security
|
||||||
|
|
||||||
import be.simplenotes.domain.usecases.login.Token
|
import be.simplenotes.domain.usecases.users.login.Token
|
||||||
import be.simplenotes.shared.config.JwtConfig
|
import be.simplenotes.shared.config.JwtConfig
|
||||||
import com.auth0.jwt.JWT
|
import com.auth0.jwt.JWT
|
||||||
import com.auth0.jwt.algorithms.Algorithm
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package be.simplenotes.domain.usecases.login
|
package be.simplenotes.domain.usecases.users.login
|
||||||
|
|
||||||
import be.simplenotes.domain.model.PersistedUser
|
import be.simplenotes.domain.model.PersistedUser
|
||||||
import be.simplenotes.domain.security.BcryptPasswordHash
|
import be.simplenotes.domain.security.BcryptPasswordHash
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package be.simplenotes.domain.usecases.register
|
package be.simplenotes.domain.usecases.users.register
|
||||||
|
|
||||||
import be.simplenotes.domain.model.PersistedUser
|
import be.simplenotes.domain.model.PersistedUser
|
||||||
import be.simplenotes.domain.security.BcryptPasswordHash
|
import be.simplenotes.domain.security.BcryptPasswordHash
|
||||||
@ -1,8 +1,8 @@
|
|||||||
package be.simplenotes.domain.validation
|
package be.simplenotes.domain.validation
|
||||||
|
|
||||||
import be.simplenotes.domain.usecases.login.InvalidLoginForm
|
import be.simplenotes.domain.usecases.users.login.InvalidLoginForm
|
||||||
import be.simplenotes.domain.usecases.login.LoginForm
|
import be.simplenotes.domain.usecases.users.login.LoginForm
|
||||||
import be.simplenotes.domain.usecases.register.RegisterForm
|
import be.simplenotes.domain.usecases.users.register.RegisterForm
|
||||||
import be.simplenotes.shared.testutils.assertions.isLeftOfType
|
import be.simplenotes.shared.testutils.assertions.isLeftOfType
|
||||||
import be.simplenotes.shared.testutils.assertions.isRight
|
import be.simplenotes.shared.testutils.assertions.isRight
|
||||||
import com.natpryce.hamkrest.assertion.assertThat
|
import com.natpryce.hamkrest.assertion.assertThat
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user