Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/models/App.scala
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ object App extends RedisModel {
case _ => None
}

def delete(key: String): Boolean = {
App.findByKey(key).map(app => {
app.delete
}).getOrElse(false)
}

def create(userId: Long, name: String, appMode: Int, debugMode: Boolean, iosCertPassword: Option[String], gcmApiKey: Option[String]): Option[String] = {
val key = RandomGenerator.generateKey(22)
val secret = RandomGenerator.generateSecret(22)
Expand Down Expand Up @@ -116,4 +122,4 @@ object RandomGenerator {
def generateRandomString(length: Int, chars: Seq[Char]) =
(1 to length map { _ => chars(random.nextInt(chars.length)) }).mkString

}
}
9 changes: 4 additions & 5 deletions app/models/DeviceToken.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package models

import java.util.Date
import com.redis._
import com.redis.serialization._
import Parse.Implicits.parseLong

Expand All @@ -13,7 +12,7 @@ case class DeviceToken(appKey: String, value: String, lastRegistrationDate: Date

object DeviceToken extends RedisConnection {

def findAllByAppKey(appKey: String, limit: Option[(Int, Int)] = None) = {
def findAllByAppKey(appKey: String, limit: Option[(Int, Int)] = None): List[DeviceToken] = {
def iterate(result: Iterable[Option[String]], acc: List[DeviceToken]): List[DeviceToken] = result match {
case Some(value) :: Some(time) :: rest => {
iterate(rest, DeviceToken(appKey, value, new Date(time.toLong)) :: acc)
Expand All @@ -27,9 +26,9 @@ object DeviceToken extends RedisConnection {
result map (iterate(_, List())) getOrElse List()
}

def findByAppKeyAndValue(appKey: String, value: String) =
def findByAppKeyAndValue(appKey: String, value: String): Option[DeviceToken] =
redis.get("device_token:" + appKey + ":" + value.toUpperCase) map { time =>
Some(DeviceToken(appKey, value, new Date(time.toLong)))
Some(DeviceToken(appKey, value, new Date(time)))
} getOrElse None

def countAllByAppKey(appKey: String): Long =
Expand All @@ -56,4 +55,4 @@ object DeviceToken extends RedisConnection {
true
}

}
}
15 changes: 9 additions & 6 deletions app/models/Event.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ case class Event(date: Date, severity: Short, message: String) {

}

object Event {
object Event extends RedisConnection {

val redis = new RedisClient("localhost", 6379)
val maxEventsCount = 1000

object Severity {
Expand All @@ -31,7 +30,7 @@ object Event {
val CRITICAL: Short = 5
}

def findAllByAppKey(appKey: String, offset: Int, count: Int) = {
def findAllByAppKey(appKey: String, offset: Int = 0, count: Int = 50): List[Event] = {
redis.lrange("events:" + appKey, offset, count) map { list =>
list.flatten map { jsonString =>
val json = Json.parse(jsonString)
Expand All @@ -43,10 +42,10 @@ object Event {
} getOrElse List()
}

def countAllByAppKey(appKey: String) =
def countAllByAppKey(appKey: String): Long =
redis.llen("events:" + appKey).getOrElse[Long](0)

def create(appKey: String, severity: Short, message: String) = {
def create(appKey: String, severity: Short, message: String): Boolean = {
val log = Json.toJson(Map(
"date" -> Json.toJson(System.currentTimeMillis()),
"severity" -> Json.toJson(severity),
Expand All @@ -56,4 +55,8 @@ object Event {
redis.ltrim("events:" + appKey, 0, maxEventsCount - 1)
}

}
def deleteByAppKey(appKey: String): Option[Long] = {
redis.del(s"events:$appKey")
}

}
6 changes: 2 additions & 4 deletions app/models/RedisModel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ package models
import com.redis._

trait RedisConnection {

val redis = new RedisClient("localhost", 6379)

}

trait RedisModel extends RedisConnection {

def createOrUpdateHash(key: String, hash: Map[String, Any]) = {
def createOrUpdateHash(key: String, hash: Map[String, Any]): Boolean = {
val noneFields = noneFieldsFromMap(hash)
val valuesHash = valuesMapFromMap(hash)

Expand All @@ -31,4 +29,4 @@ trait RedisModel extends RedisConnection {
def valuesMapFromMap(m: Map[String, Any]) =
(m -- noneFieldsFromMap(m)) map { case (k, Some(v)) => k -> v; case (k, v) => k -> v }

}
}
60 changes: 20 additions & 40 deletions app/models/User.scala
Original file line number Diff line number Diff line change
@@ -1,65 +1,45 @@
package models

case class User(email: String, encryptedPassword: String, passwordSalt: String)
import com.github.t3hnar.bcrypt._

case class User(email: String, encryptedPassword: String)

object User extends RedisModel {

def fromMap(attrs: Map[String, String]): User = {
User(attrs.getOrElse("email", ""),
attrs.getOrElse("encryptedPassword", ""),
attrs.getOrElse("passwordSalt", ""))
attrs.getOrElse("encryptedPassword", ""))
}

def authenticate(email: String, password: String) = User.findByEmail(email) map { user =>
PasswordUtil.authenticate(password, PasswordUtil.toByteArray(user.encryptedPassword), PasswordUtil.toByteArray(user.passwordSalt))
def authenticate(email: String, password: String): Boolean = User.findByEmail(email) map { user =>
PasswordUtil.authenticate(password, user.encryptedPassword)
} getOrElse false

def findByEmail(email: String) = redis.hgetall("user:" + email) map { m =>
def findByEmail(email: String): Option[User] = redis.hgetall("user:" + email) map { m =>
if (m.nonEmpty) Some(fromMap(m)) else None
} getOrElse None

def create(email: String, password: String) = {
val passwordSalt = PasswordUtil.generateSalt
val encryptedPassword = PasswordUtil.encryptPassword(password, passwordSalt)
val attrs = Map("email" -> email, "encryptedPassword" -> PasswordUtil.toString(encryptedPassword), "passwordSalt" -> PasswordUtil.toString(passwordSalt))
def create(email: String, password: String): Boolean = {
val encryptedPassword = PasswordUtil.encryptPassword(password)
val attrs = Map("email" -> email, "encryptedPassword" -> encryptedPassword)
createOrUpdateHash("user:" + email, attrs)
}

def delete(email: String): Option[Long] = {
redis.del(s"user:$email")
}

}

// http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html
// http://codahale.com/how-to-safely-store-a-password/
object PasswordUtil {

import java.nio.charset.Charset
import java.security.{NoSuchAlgorithmException, SecureRandom}
import java.security.spec.{InvalidKeySpecException, KeySpec}
import java.util.Arrays

import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec

def authenticate(attemptedPassword: String, encryptedPassword: Array[Byte], salt: Array[Byte]) = {
val encryptedAttemptedPassword = encryptPassword(attemptedPassword, salt)
Arrays.equals(encryptedPassword, encryptedAttemptedPassword)
def authenticate(attemptedPassword: String, encryptedPassword: String) = {
attemptedPassword.isBcrypted(encryptedPassword)
}

def encryptPassword(password: String, salt: Array[Byte]) = {
val spec = new PBEKeySpec(password.toCharArray, salt, 10000, 160)
val f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
f.generateSecret(spec).getEncoded
def encryptPassword(password: String) = {
password.bcrypt
}

def generateSalt = {
val random = SecureRandom.getInstance("SHA1PRNG")
val salt: Array[Byte] = Array(0, 0, 0, 0, 0, 0, 0, 0)
random.nextBytes(salt)
salt
}

def toString(bytes: Array[Byte]) =
new sun.misc.BASE64Encoder().encode(bytes)

def toByteArray(s: String) =
new sun.misc.BASE64Decoder().decodeBuffer(s)

}
}
5 changes: 4 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ name := "plush"
version := "1.1-SNAPSHOT"

libraryDependencies := Seq(
"net.debasishg" %% "redisclient" % "2.10"
"net.debasishg" %% "redisclient" % "2.10",
"com.github.t3hnar" %% "scala-bcrypt" % "2.4",
"org.scalatest" % "scalatest_2.10" % "2.0" % "test",
"org.scalatestplus" %% "play" % "1.0.0" % "test"
)

scalacOptions := Seq("-feature")
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=0.13.0
sbt.version=0.13.6
42 changes: 42 additions & 0 deletions test/models/AppSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package models

import org.scalatestplus.play.{OneAppPerSuite, PlaySpec}

class AppSpec extends PlaySpec with OneAppPerSuite {
val testUserId = 123L
val testAppName = "testAppName"
val testAppMode = 0
val testAppDebugMode = true
val testAppIosCertPass = Some("testIosCertPass")
val testAppGcmKey = Some("testGcmKey")

var testApp: Option[App] = None
var testAppKey: Option[String] = None

"App" must {
"create an app" in {
val created = App.create(testUserId, testAppName, testAppMode, testAppDebugMode, testAppIosCertPass, testAppGcmKey)
assert(created.nonEmpty)
testAppKey = created
}

"find an app by key" in {
val found = App.findByKey(testAppKey.get)
assert(found.nonEmpty)
assert(found.get.name == testAppName)
assert(found.get.userId == testUserId)
assert(found.get.iosCertPassword == testAppIosCertPass)
assert(found.get.gcmApiKey == testAppGcmKey)
}

"find all" in {
val found = App.all
assert(found.nonEmpty)
assert(found.head.name == testAppName)
assert(found.head.userId == testUserId)
assert(found.head.iosCertPassword == testAppIosCertPass)
assert(found.head.gcmApiKey == testAppGcmKey)
}

}
}
44 changes: 44 additions & 0 deletions test/models/DeviceTokenSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package models

import org.scalatestplus.play.{OneAppPerSuite, PlaySpec}
import play.api.Play


class DeviceTokenSpec extends PlaySpec with OneAppPerSuite {
val testAppKey = "testAppKey"
val testValue = "testValue"

"DeviceToken" must {

"create a device token" in {
val created = DeviceToken.create(testAppKey, testValue)
assert(created.nonEmpty)
assert(created.get.appKey == testAppKey)
assert(created.get.value == testValue.toUpperCase)
}

"count all by app key" in {
val count = DeviceToken.countAllByAppKey(testAppKey)
assert(count == 1L)
}

"find all by app key" in {
val found = DeviceToken.findAllByAppKey(testAppKey)
assert(found.length == 1)
assert(found.head.appKey == testAppKey)
assert(found.head.value == testValue.toUpperCase)
}

"find by app key and value" in {
val found = DeviceToken.findByAppKeyAndValue(testAppKey, testValue)
assert(found.nonEmpty)
assert(found.get.appKey == testAppKey)
assert(found.get.value == testValue)
}

"delete a device token" in {
val deleted = DeviceToken.delete(testAppKey, testValue)
assert(deleted)
}
}
}
42 changes: 42 additions & 0 deletions test/models/EventSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package models

import org.scalatestplus.play.{OneAppPerSuite, PlaySpec}
import play.api.Play


class EventSpec extends PlaySpec with OneAppPerSuite {
val testAppKey = "testAppKey"
val testSeverity = 0.toShort
val testMessage = "testMessage"

"Event" must {
"start the FakeApplication" in {
Play.maybeApplication mustBe Some(app)
}

"create an event" in {
val created = Event.create(testAppKey, testSeverity, testMessage)
assert(created)
}


"count all by app key" in {
val count = Event.countAllByAppKey(testAppKey)
assert(count == 1)
}

"find all by app key" in {
val found = Event.findAllByAppKey(testAppKey)
assert(found.length == 1)
assert(found.head.message == testMessage)
assert(found.head.severity == testSeverity)
}

"remove by app key" in {
val removed = Event.deleteByAppKey(testAppKey)
assert(removed.nonEmpty)
assert(removed.get == 1L)
}

}
}
Loading