Skip to content

Commit

Permalink
Merge pull request #3643 from ProjectSidewalk/develop
Browse files Browse the repository at this point in the history
v7.20.5
  • Loading branch information
misaugstad authored Sep 3, 2024
2 parents d15deae + 9d44e41 commit bb9cde5
Show file tree
Hide file tree
Showing 145 changed files with 750 additions and 265 deletions.
1 change: 1 addition & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ module.exports = function(grunt) {
'public/javascripts/Gallery/src/cards/*.js',
'public/javascripts/Gallery/src/data/*.js',
'public/javascripts/Gallery/src/filter/*.js',
'public/javascripts/Gallery/src/keyboard/*.js',
'public/javascripts/Gallery/src/validation/*.js',
'public/javascripts/Gallery/src/displays/*.js',
'public/javascripts/Gallery/src/modal/*.js',
Expand Down
179 changes: 95 additions & 84 deletions app/controllers/SignUpController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ class SignUpController @Inject() (
extends Silhouette[User, SessionAuthenticator] with ProvidesHeader {


/**
* Helper function to check if username contain invalid characters.
*/
private def containsInvalidCharacters(username: String): Boolean = {
username.exists(c => c == '"' || c == '\'' || c == '<' || c == '>' || c == '&')
}

/**
* Registers a new user.
*/
Expand All @@ -60,91 +67,95 @@ class SignUpController @Inject() (
// If they are getting community service hours, make sure they redirect to the instructions page.
val serviceHoursUser: Boolean = data.serviceHours == Messages("yes.caps")
val nextUrl: Option[String] = if (serviceHoursUser && url.isDefined) Some("/serviceHoursInstructions") else url
// Check presence of user by username.
UserTable.find(data.username) match {
case Some(user) =>
WebpageActivityTable.save(WebpageActivity(0, oldUserId, ipAddress, "Duplicate_Username_Error", timestamp))
// Future.successful(Redirect(routes.UserController.signUp()).flashing("error" -> Messages("authenticate.error.username.exists")))
Future.successful(Status(409)(Messages("authenticate.error.username.exists")))
case None =>

// Check presence of user by email.
UserTable.findEmail(data.email.toLowerCase) match {
case Some(user) =>
WebpageActivityTable.save(WebpageActivity(0, oldUserId, ipAddress, "Duplicate_Email_Error", timestamp))
// Future.successful(Redirect(routes.UserController.signUp()).flashing("error" -> Messages("authenticate.error.email.exists")))
Future.successful(Status(409)(Messages("authenticate.error.email.exists")))
case None =>
// Check if passwords match and are at least 6 characters.
if (data.password != data.passwordConfirm) {
Future.successful(Redirect(routes.UserController.signUp()).flashing("error" -> Messages("authenticate.error.password.mismatch")))
} else if (data.password.length < 6) {
Future.successful(Redirect(routes.UserController.signUp()).flashing("error" -> Messages("authenticate.error.password.length")))
} else {
val authInfo = passwordHasher.hash(data.password)
val user = User(
userId = request.identity.map(_.userId).getOrElse(UUID.randomUUID()),
loginInfo = LoginInfo(CredentialsProvider.ID, data.email.toLowerCase),
username = data.username,
email = data.email.toLowerCase,
role = None
)

val newAuthenticator: Future[SessionAuthenticator] = for {
user <- userService.save(user)
authInfo <- authInfoService.save(user.loginInfo, authInfo)
authenticator <- env.authenticatorService.create(user.loginInfo)
} yield {
// Set the user role, assign the neighborhood to audit, and add to the user_stat table.
UserRoleTable.setRole(user.userId, "Registered", Some(serviceHoursUser))
UserCurrentRegionTable.assignRegion(user.userId)
UserStatTable.addUserStatIfNew(user.userId)

// Log the sign up/in.
val timestamp: Timestamp = new Timestamp(Instant.now.toEpochMilli)
WebpageActivityTable.save(WebpageActivity(0, user.userId.toString, ipAddress, "SignUp", timestamp))
WebpageActivityTable.save(WebpageActivity(0, user.userId.toString, ipAddress, "SignIn", timestamp))

env.eventBus.publish(SignUpEvent(user, request, request2lang))
env.eventBus.publish(LoginEvent(user, request, request2lang))
authenticator
}
request.authenticator match {
case Some(oldAuthenticator) =>
// If someone was already authenticated (i.e., they were signed into an anon user account), Play
// doesn't let us sign one account out and the other back in in one response header. So we start
// by redirecting to the "/finishSignUp" endpoint, discarding the old authenticator info and
// sending the new authenticator info in a temp element in the session cookie. The "/finishSignUp"
// endpoint will then move authenticator we put in "temp-authenticator" over to "authenticator"
// where it belongs, finally completing the sign up.
val redirectURL: String = nextUrl match {
case Some(u) => "/finishSignUp?url=" + u
case None => "/finishSignUp"
}
val result = newAuthenticator.map { newAuth =>
// When we encrypt/serialize, we're doing the work of init, serialize, and embed. Found here:
// https://github.com/mohiva/play-silhouette/blob/2.0.x/silhouette/app/com/mohiva/play/silhouette/impl/authenticators/SessionAuthenticator.scala
val authSerialized: String = Crypto.encryptAES(Json.toJson(newAuth).toString())
Redirect(redirectURL).withSession("temp-authenticator" -> authSerialized)
}
oldAuthenticator.discard(result)

case None =>
// If no account was signed in before, we can skip the "/finishSignUp" endpoint step. Instead, we
// embed the authenticator into the session cookie in the normal way and either forward to the
// new URL or simply send the newly authenticated user object.
val result = nextUrl match {
case Some(u) => Future.successful(Redirect(u))
case None => Future.successful(Ok(Json.toJson(user)))
}
for {
newA <- newAuthenticator
value <- env.authenticatorService.init(newA)
resultWithAuth <- env.authenticatorService.embed(value, result)
} yield resultWithAuth
if (containsInvalidCharacters(data.username)) {
WebpageActivityTable.save(WebpageActivity(0, oldUserId, ipAddress, "Invalid_Username_Characters_Error", timestamp))
Future.successful(Status(400)(Messages("authenticate.error.username.invalid")))
} else {
// Check presence of user by username.
UserTable.find(data.username) match {
case Some(user) =>
WebpageActivityTable.save(WebpageActivity(0, oldUserId, ipAddress, "Duplicate_Username_Error", timestamp))
// Future.successful(Redirect(routes.UserController.signUp()).flashing("error" -> Messages("authenticate.error.username.exists")))
Future.successful(Status(409)(Messages("authenticate.error.username.exists")))
case None =>
// Check presence of user by email.
UserTable.findEmail(data.email.toLowerCase) match {
case Some(user) =>
WebpageActivityTable.save(WebpageActivity(0, oldUserId, ipAddress, "Duplicate_Email_Error", timestamp))
// Future.successful(Redirect(routes.UserController.signUp()).flashing("error" -> Messages("authenticate.error.email.exists")))
Future.successful(Status(409)(Messages("authenticate.error.email.exists")))
case None =>
// Check if passwords match and are at least 6 characters.
if (data.password != data.passwordConfirm) {
Future.successful(Redirect(routes.UserController.signUp()).flashing("error" -> Messages("authenticate.error.password.mismatch")))
} else if (data.password.length < 6) {
Future.successful(Redirect(routes.UserController.signUp()).flashing("error" -> Messages("authenticate.error.password.length")))
} else {
val authInfo = passwordHasher.hash(data.password)
val user = User(
userId = request.identity.map(_.userId).getOrElse(UUID.randomUUID()),
loginInfo = LoginInfo(CredentialsProvider.ID, data.email.toLowerCase),
username = data.username,
email = data.email.toLowerCase,
role = None
)

val newAuthenticator: Future[SessionAuthenticator] = for {
user <- userService.save(user)
authInfo <- authInfoService.save(user.loginInfo, authInfo)
authenticator <- env.authenticatorService.create(user.loginInfo)
} yield {
// Set the user role, assign the neighborhood to audit, and add to the user_stat table.
UserRoleTable.setRole(user.userId, "Registered", Some(serviceHoursUser))
UserCurrentRegionTable.assignRegion(user.userId)
UserStatTable.addUserStatIfNew(user.userId)

// Log the sign up/in.
val timestamp: Timestamp = new Timestamp(Instant.now.toEpochMilli)
WebpageActivityTable.save(WebpageActivity(0, user.userId.toString, ipAddress, "SignUp", timestamp))
WebpageActivityTable.save(WebpageActivity(0, user.userId.toString, ipAddress, "SignIn", timestamp))

env.eventBus.publish(SignUpEvent(user, request, request2lang))
env.eventBus.publish(LoginEvent(user, request, request2lang))
authenticator
}
request.authenticator match {
case Some(oldAuthenticator) =>
// If someone was already authenticated (i.e., they were signed into an anon user account), Play
// doesn't let us sign one account out and the other back in in one response header. So we start
// by redirecting to the "/finishSignUp" endpoint, discarding the old authenticator info and
// sending the new authenticator info in a temp element in the session cookie. The "/finishSignUp"
// endpoint will then move authenticator we put in "temp-authenticator" over to "authenticator"
// where it belongs, finally completing the sign up.
val redirectURL: String = nextUrl match {
case Some(u) => "/finishSignUp?url=" + u
case None => "/finishSignUp"
}
val result = newAuthenticator.map { newAuth =>
// When we encrypt/serialize, we're doing the work of init, serialize, and embed. Found here:
// https://github.com/mohiva/play-silhouette/blob/2.0.x/silhouette/app/com/mohiva/play/silhouette/impl/authenticators/SessionAuthenticator.scala
val authSerialized: String = Crypto.encryptAES(Json.toJson(newAuth).toString())
Redirect(redirectURL).withSession("temp-authenticator" -> authSerialized)
}
oldAuthenticator.discard(result)

case None =>
// If no account was signed in before, we can skip the "/finishSignUp" endpoint step. Instead, we
// embed the authenticator into the session cookie in the normal way and either forward to the
// new URL or simply send the newly authenticated user object.
val result = nextUrl match {
case Some(u) => Future.successful(Redirect(u))
case None => Future.successful(Ok(Json.toJson(user)))
}
for {
newA <- newAuthenticator
value <- env.authenticatorService.init(newA)
resultWithAuth <- env.authenticatorService.embed(value, result)
} yield resultWithAuth
}
}
}
}
}
}
}
}
)
Expand Down
6 changes: 6 additions & 0 deletions app/controllers/ValidationTaskController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ class ValidationTaskController @Inject() (implicit val env: Environment[User, Se
val mission: Mission =
MissionTable.resumeOrCreateNewValidationMission(userId, 0.0D, 0.0D, "labelmapValidation", labelTypeId).get

// Check if user already has a validation for this label.
if(LabelValidationTable.countValidationsFromUserAndLabel(userId, submission.labelId) != 0) {
// Delete the user's old label.
LabelValidationTable.deleteLabelValidation(submission.labelId, userId.toString)
}

// Insert a label_validation entry for this label.
val newValId: Int = LabelValidationTable.insert(LabelValidation(0, submission.labelId,
submission.validationResult, submission.oldSeverity, submission.newSeverity, submission.oldTags,
Expand Down
6 changes: 3 additions & 3 deletions app/formats/json/APIFormats.scala
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ object APIFormats {
s"""${LabelPointTable.canvasWidth},${LabelPointTable.canvasHeight},"${l.gsvUrl}",${l.imageLabelDates._1},""" +
s"${l.imageLabelDates._2},${l.labelSeverity.getOrElse("NA")},${l.labelTemporary}," +
s"${l.agreeDisagreeUnsureCount._1},${l.agreeDisagreeUnsureCount._2},${l.agreeDisagreeUnsureCount._3}," +
s""""[${l.labelTags.mkString(",")}]","${l.labelDescription.getOrElse("NA")}",${l.userId}"""
s""""[${l.labelTags.mkString(",")}]","${l.labelDescription.getOrElse("NA").replace("\"", "\"\"")}",${l.userId}"""
}

def rawLabelMetadataToJSON(l: LabelAllMetadata): JsObject = {
Expand Down Expand Up @@ -245,8 +245,8 @@ object APIFormats {

def rawLabelMetadataToCSVRow(l: LabelAllMetadata): String = {
s"${l.labelId},${l.geom.lat},${l.geom.lng},${l.userId},${l.panoId},${l.labelType},${l.severity.getOrElse("NA")}," +
s""""[${l.tags.mkString(",")}]",${l.temporary},"${l.description.getOrElse("NA")}",${l.timeCreated},""" +
s"${l.streetEdgeId},${l.osmStreetId},${l.neighborhoodName},${l.validationInfo.correct.getOrElse("NA")}," +
s""""[${l.tags.mkString(",")}]",${l.temporary},"${l.description.getOrElse("NA").replace("\"", "\"\"")}",""" +
s"${l.timeCreated},${l.streetEdgeId},${l.osmStreetId},${l.neighborhoodName},${l.validationInfo.correct.getOrElse("NA")}," +
s"${l.validationInfo.agreeCount},${l.validationInfo.disagreeCount},${l.validationInfo.unsureCount}," +
s""""[${l.validations.map(v => s"{user_id: ${v._1}, validation: ${LabelValidationTable.validationOptions(v._2)}")}]",""" +
s"${l.auditTaskId},${l.missionId},${l.imageCaptureDate},${l.pov.heading},${l.pov.pitch},${l.pov.zoom}," +
Expand Down
2 changes: 1 addition & 1 deletion app/models/label/LabelHistory.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import scala.slick.lifted.ForeignKeyQuery

case class LabelHistory(labelHistoryId: Int, labelId: Int, severity: Option[Int], tags: List[String], editedBy: String,
editTime: Timestamp, source: String, labelValidationId: Option[Int]) {
require(List("Explore", "ValidateDesktop", "ValidateDesktopNew", "ValidateMobile", "LabelMap", "GalleryImage", "GalleryExpandedImage", "GalleryThumbs", "AdminUserDashboard", "AdminLabelSearchTab").contains(source), "Invalid source for Label History table.")
require(List("Explore", "ValidateDesktop", "ValidateDesktopNew", "ValidateMobile", "LabelMap", "GalleryImage", "GalleryExpandedImage", "GalleryThumbs", "AdminUserDashboard", "AdminLabelSearchTab", "ExternalTagValidationASSETS2024").contains(source), "Invalid source for Label History table.")
}

class LabelHistoryTable(tag: slick.lifted.Tag) extends Table[LabelHistory](tag, "label_history") {
Expand Down
16 changes: 15 additions & 1 deletion app/models/label/LabelValidationTable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import models.user.{RoleTable, UserRoleTable, UserStatTable}
import play.api.Play.current
import play.api.libs.json.{JsObject, Json}
import scala.slick.jdbc.{StaticQuery => Q}
import scala.slick.lifted.ForeignKeyQuery
import scala.slick.lifted.{ForeignKeyQuery, Index}

case class LabelValidation(labelValidationId: Int,
labelId: Int,
Expand Down Expand Up @@ -71,6 +71,8 @@ class LabelValidationTable (tag: slick.lifted.Tag) extends Table[LabelValidation

def mission: ForeignKeyQuery[MissionTable, Mission] =
foreignKey("label_validation_mission_id_fkey", missionId, TableQuery[MissionTable])(_.missionId)

def userLabelUnique: Index = index("label_validation_user_id_label_id_unique", (userId, labelId), unique = true)
}

/**
Expand All @@ -87,6 +89,18 @@ object LabelValidationTable {

val validationOptions: Map[Int, String] = Map(1 -> "Agree", 2 -> "Disagree", 3 -> "Unsure")

/**
* A function to count all validations by the given user for the given label.
* There should only ever be a maximum of one.
*
* @param userId
* @param labelId The ID of the label
* @return An integer with the count
*/
def countValidationsFromUserAndLabel(userId: UUID, labelId: Int): Int = db.withSession { implicit session =>
validationLabels.filter(v => v.userId === userId.toString && v.labelId === labelId).length.run
}

/**
* Returns how many agree, disagree, or unsure validations a user entered for a given mission.
*
Expand Down
10 changes: 5 additions & 5 deletions app/views/gallery.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@
@* These are templates and are not visible on the screen directly. *@
@* JS clones them, adjusts the attributes and then appends to DOM. *@
<div class="severity-filter-image severity-1 template">
<svg viewBox="0 0 150 150"><use xlink:href="#smiley-neutral"></use></svg>
<svg viewBox="0 0 155 155"><use xlink:href="#smiley-neutral"></use></svg>
</div>
<div class="severity-filter-image severity-2 template">
<svg viewBox="0 0 150 150"><use xlink:href="#smiley-frown-1"></use></svg>
<svg viewBox="0 0 155 155"><use xlink:href="#smiley-frown-1"></use></svg>
</div>
<div class="severity-filter-image severity-3 template">
<svg viewBox="0 0 150 150"><use xlink:href="#smiley-frown-2"></use></svg>
<svg viewBox="0 0 155 155"><use xlink:href="#smiley-frown-2"></use></svg>
</div>
<div class="severity-filter-image severity-4 template">
<svg viewBox="0 0 150 150"><use xlink:href="#smiley-frown-3"></use></svg>
<svg viewBox="0 0 155 155"><use xlink:href="#smiley-frown-3"></use></svg>
</div>
<div class="severity-filter-image severity-5 template">
<svg viewBox="0 0 150 150"><use xlink:href="#smiley-frown-4"></use></svg>
<svg viewBox="0 0 155 155"><use xlink:href="#smiley-frown-4"></use></svg>
</div>

<div id="gallery">
Expand Down
3 changes: 3 additions & 0 deletions app/views/navbar.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,9 @@ <h4 class="modal-title" id="sign-up-label">@Messages("authenticate.signup")</h4>
errorMessage = "@Messages("authenticate.error.username.exists")"
}
}
else if(e.status === 400){
errorMessage = e.responseText
}
else { // Some other Bad Request.
errorMessage = '@Messages("authenticate.error.generic")'
}
Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import play.PlayScala

name := """sidewalk-webpage"""

version := "7.20.4"
version := "7.20.5"

scalaVersion := "2.10.7"

Expand Down
Loading

0 comments on commit bb9cde5

Please sign in to comment.