Skip to content

Instantly share code, notes, and snippets.

@nbenns
Last active July 7, 2020 23:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nbenns/1f3fa4e8395caaa91c4bfb02627722dc to your computer and use it in GitHub Desktop.
Save nbenns/1f3fa4e8395caaa91c4bfb02627722dc to your computer and use it in GitHub Desktop.
Making sure you don't screw up passwords via Phantom types
sealed trait SecurityState
trait PlainText extends SecurityState
trait Hashed extends SecurityState
trait Confirmed extends SecurityState
trait Verified extends SecurityState
sealed trait UpdateState
trait New extends UpdateState
trait Confirmation extends UpdateState
trait Current extends UpdateState
trait Provided extends UpdateState
sealed trait PasswordError extends Throwable with Product with Serializable
final case class PasswordInvalidError() extends PasswordError
final case class PasswordsDontMatchError() extends PasswordError
sealed abstract class Password[A <: UpdateState, S <: SecurityState] private(val value: String)
object Password {
private def apply[A <: UpdateState, S <: SecurityState](value: String): Password[A, S] =
new Password[A, S](value) {}
def setCurrent(ev: Password[Provided, Verified], p: Password[New, Confirmed]): Password[Current, Hashed] =
Password(p.value)
def hash[A <: UpdateState](p: Password[A, PlainText]): Either[Nothing, Password[A, Hashed]] =
Right(Password(p.value))
def confirm(p1: Password[New, Hashed], p2: Password[Confirmation, Hashed]): Either[PasswordsDontMatchError, Password[New, Confirmed]] =
if (p1.value == p2.value) Right(Password(p1.value))
else Left(PasswordsDontMatchError())
def verify(p1: Password[Current, Hashed], p2: Password[Provided, Hashed]): Either[PasswordInvalidError, Password[Provided, Verified]] =
if (p1.value == p2.value) Right(Password(p2.value))
else Left(PasswordInvalidError())
}
case class ChangePasswordRequest(
email: String,
currentPassword: Password[Provided, PlainText],
newPassword: Password[New, PlainText],
confirmationPassword: Password[Confirmation, PlainText]
)
case class User(id: Long, email: String, password: Password[Current, Hashed]) {
def updatePassword(req: ChangePasswordRequest): Either[PasswordError, User] =
for {
currentPassword <- Password.hash(req.currentPassword)
verifiedPassword <- Password.verify(password, currentPassword)
newPassword <- Password.hash(req.newPassword)
confirmationPassword <- Password.hash(req.confirmationPassword)
confirmedPassword <- Password.confirm(newPassword, confirmationPassword)
updatedPassword = Password.setCurrent(verifiedPassword, confirmedPassword)
} yield this.copy(password = updatedPassword)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment