Issue with user account activation


#1

Hello,

I am using Silhouette seed project (https://github.com/mohiva/play-silhouette-seed) together with Silhouette Persistence ReactiveMongo (https://github.com/mohiva/play-silhouette-persistence-reactivemongo) and recieveing an error when user is trying to activate an account.
P.S. There are no errors/warnings during the compiliation.

! @79c262ep5 - Internal server error, for (GET) [/account/activate/e82e0354-a243-411a-8f32-6053ce2c54ca] ->

play.api.http.HttpErrorHandlerExceptions$$anon$1: Execution exception[[JsResultException: JsResultException(errors:List((/id,List(JsonValidationError(List(error.path.missing),WrappedArray())))))]]
        at play.api.http.HttpErrorHandlerExceptions$.throwableToUsefulException(HttpErrorHandler.scala:251)
        at play.api.http.DefaultHttpErrorHandler.onServerError(HttpErrorHandler.scala:178)
        at play.core.server.AkkaHttpServer$$anonfun$1.applyOrElse(AkkaHttpServer.scala:363)
        at play.core.server.AkkaHttpServer$$anonfun$1.applyOrElse(AkkaHttpServer.scala:361)
        at scala.concurrent.Future.$anonfun$recoverWith$1(Future.scala:413)
        at scala.concurrent.impl.Promise.$anonfun$transformWith$1(Promise.scala:37)
        at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:60)
        at akka.dispatch.BatchingExecutor$AbstractBatch.processBatch(BatchingExecutor.scala:55)
        at akka.dispatch.BatchingExecutor$BlockableBatch.$anonfun$run$1(BatchingExecutor.scala:91)
        at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:12)
Caused by: play.api.libs.json.JsResultException: JsResultException(errors:List((/id,List(JsonValidationError(List(error.path.missing),WrappedArray())))))
        at reactivemongo.play.json.JSONSerializationPack$.deserialize(JSONSerializationPack.scala:61)
        at reactivemongo.play.json.JSONSerializationPack$.deserialize(JSONSerializationPack.scala:33)
        at reactivemongo.core.protocol.ReplyDocumentIterator$.$anonfun$parse$1(protocol.scala:141)
        at scala.collection.Iterator$$anon$10.next(Iterator.scala:457)
        at scala.collection.Iterator$ConcatIterator.next(Iterator.scala:227)
        at reactivemongo.api.DefaultCursor$Impl.$anonfun$headOption$2(DefaultCursor.scala:332)
        at scala.concurrent.Future$.$anonfun$apply$1(Future.scala:654)
        at scala.util.Success.$anonfun$map$1(Try.scala:251)
        at scala.util.Success.map(Try.scala:209)
        at scala.concurrent.Future.$anonfun$map$1(Future.scala:288)

AuthTokenDAOImpl.scala looks like this:

/**
 * Give access to the [[AuthToken]] object.
 */
class AuthTokenDAOImpl @Inject() (val reactiveMongoApi: ReactiveMongoApi) extends AuthTokenDAO {

  def collection: Future[JSONCollection] = reactiveMongoApi.database.map(_.collection("silhouette.token"))

  def find(id: UUID): Future[Option[AuthToken]] = {
    val query = Json.obj("id" -> id)
    collection.flatMap(_.find(query, projection = Some(Json.obj("id" -> 0))).one[AuthToken])
  }
 
  def findExpired(dateTime: DateTime): Future[Seq[AuthToken]] = {
    val query = Json.obj("expiry" -> Json.obj("$lt" -> dateTime))
    collection.flatMap(_.find(query, projection = Some(Json.obj("expiry" -> 0)))
      .cursor[AuthToken](readPreference = ReadPreference.primary)
      .collect[Seq](100, Cursor.FailOnError[Seq[AuthToken]]())
    )
  }
  
  def save(token: AuthToken): Future[AuthToken] = {
    collection.flatMap(_.insert(token))
    Future.successful(token)
  }
 
  def remove(id: UUID) = onSuccess(collection.flatMap(_.delete().one(Json.obj("_id" -> id))), ())

  /**
   * Returns some result on success and None on error.
   *
   * @param result The last result.
   * @param entity The entity to return.
   * @tparam T The type of the entity.
   * @return The entity on success or an exception on error.
   */
  private def onSuccess[T](result: Future[WriteResult], entity: T): Future[T] = result.recoverWith {
    case e => Future.failed(new MongoException("Got exception from MongoDB", e.getCause))
  }.map { r =>
    WriteResult.lastError(r) match {
      case Some(e) => throw new MongoException(e.message, e)
      case _ => entity
    }
  }

}

AuthTokenServiceImpl.scala

class AuthTokenServiceImpl @Inject() (
  authTokenDAO: AuthTokenDAO,
  clock: Clock
)(
  implicit
  ex: ExecutionContext
) extends AuthTokenService {
  
  def create(userID: UUID, expiry: FiniteDuration = 5 minutes) = {
    val token = AuthToken(UUID.randomUUID(), userID, clock.now.withZone(DateTimeZone.UTC).plusSeconds(expiry.toSeconds.toInt))
    authTokenDAO.save(token)
  }
  
  def validate(id: UUID) = authTokenDAO.find(id)
  
  def clean = authTokenDAO.findExpired(clock.now.withZone(DateTimeZone.UTC)).flatMap { tokens =>
    Future.sequence(tokens.map { token =>
      authTokenDAO.remove(token.id).map(_ => token)
    })
  }
}

AuthToken.scala

case class AuthToken(id: UUID, userID: UUID, expiry: DateTime)

object AuthToken {
  implicit val jsonFormat: OFormat[AuthToken] = Json.format[AuthToken]
}

Activate account method in ActivateAccountController.scala

def activate(token: UUID) = silhouette.UnsecuredAction.async { implicit request: Request[AnyContent] =>
    authTokenService.validate(token).flatMap {
      case Some(authToken) => userService.retrieve(authToken.userID).flatMap {
        case Some(user) if user.loginInfo.providerID == CredentialsProvider.ID =>
          userService.save(user.copy(activated = true)).map { _ =>
            Redirect(routes.SignInController.view()).flashing("success" -> Messages("account.activated"))
          }
        case _ => Future.successful(Redirect(routes.SignInController.view()).flashing("error" -> Messages("invalid.activation.link")))
      }
      case None => Future.successful(Redirect(routes.SignInController.view()).flashing("error" -> Messages("invalid.activation.link")))
    }
  }

build.sbt libraries:

libraryDependencies ++= Seq(
  "com.mohiva" %% "play-silhouette" % "5.0.6",
  "com.mohiva" %% "play-silhouette-password-bcrypt" % "5.0.6",
  "com.mohiva" %% "play-silhouette-persistence" % "5.0.6",
  "com.mohiva" %% "play-silhouette-crypto-jca" % "5.0.6",
  "org.webjars" %% "webjars-play" % "2.6.3",
  "org.webjars" % "bootstrap" % "3.3.7-1" exclude("org.webjars", "jquery"),
  "org.webjars" % "jquery" % "3.2.1",
  "net.codingwell" %% "scala-guice" % "4.2.1",
  "com.iheart" %% "ficus" % "1.4.3",
  "com.typesafe.play" %% "play-mailer" % "6.0.1",
  "com.typesafe.play" %% "play-mailer-guice" % "6.0.1",
  "com.enragedginger" %% "akka-quartz-scheduler" % "1.6.1-akka-2.5.x",
  "com.adrianhurt" %% "play-bootstrap" % "1.4-P26-B3-SNAPSHOT",
  "com.mohiva" %% "play-silhouette-testkit" % "5.0.6" % "test",
  "org.reactivemongo" %% "play2-reactivemongo" % "0.16.0-play26",
  "com.typesafe.play" %% "play-json" % "2.6.10",
  "com.typesafe.play" % "play-json-joda_2.12" % "2.6.10",
  "com.mohiva" %% "play-silhouette-persistence-reactivemongo" % "5.0.6",
  specs2 % Test,
  ehcache,
  guice,
  filters
)

Any ideas what could be wrong ? Seems like instead of /id it got an List, but it does not seem that I am somwhere defining a List :(.


#2

Any ideas what could be wrong ? Seems like instead of /id it got an List, but it does not seem that I am somwhere defining a List :(.

You do not get a List. The List that is shown is the validation error.

How does you DB entry look like? Does it look similar to this?

{ 
    "_id" : "7cb5259d-3ef5-40a9-87e0-ecef81c15bae", 
    ...
}

Or does it look like:

{ 
   "_id": ObjectId("590998e65e00005e0095f1ce"),
    "id" : "7cb5259d-3ef5-40a9-87e0-ecef81c15bae", 
    ...
}

Best regards,
Christian


#3

Thanks for fast reply Christian!

It looks like this in DB:

{ 
   "_id": ObjectId("590998e65e00005e0095f1ce"),
    "id" : "7cb5259d-3ef5-40a9-87e0-ecef81c15bae", 
    ...
}

#4

Hi,

and sorry for the late response.

In your query you say that MongoDB shouldn’t return the field id and Play JSON says then that the field is missing.

collection.flatMap(_.find(query, projection = Some(Json.obj("id" -> 0))).one[AuthToken])

If you set a field to 0 in your projection clause, then MongoDB will not return this field in your result. Anyway, in your DAO you mix the ID’s sometimes you use id and sometimes you use _id.

I would suggest that you only use the field ‘_id’ in the database and the field id in you model. You can transform this field with your JSON Reads and Writes.

In the linked Projekt you can also find a complete DAO implementation.

The structure in your DB looks then like:

{ 
    "_id" : "7cb5259d-3ef5-40a9-87e0-ecef81c15bae", 
    ...
}

Best regards,
Christian


#5

Thank you for detailed answer and links to examples. I managed to solve this issue.

On additional note - it would be great if one of the seed projects which uses reactivemongo library would be updated with the latest “play2-reactivemongo” % “0.16.0-play26” version as there are some items which are deprecated from the previous versions and current seed project (https://github.com/setusoft/silhouette-play-react-seed) compiles with bunch of errors and it’s no so straightforward to fix them.

Again, thanks for keeping up to date this project. Keep up the good work. :+1: