Skip to content

Commit b52aae4

Browse files
authored
Merge pull request #33 from Hub-of-all-Things/2.4.0
2.4.0 ready for use
2 parents f056435 + 9835445 commit b52aae4

File tree

143 files changed

+6032
-2607
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

143 files changed

+6032
-2607
lines changed

.gitmodules

-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
[submodule "hat-database-schema"]
22
path = hat/conf/evolutions/hat-database-schema
33
url = https://github.com/Hub-of-all-Things/hat-database-schema.git
4-
[submodule "hat-client-scala-play"]
5-
path = hat-client-scala-play
6-
url = git@github.com:Hub-of-all-Things/hat-client-scala-play.git
7-
[submodule "marketsquare-client-scala-play"]
8-
path = marketsquare-client-scala-play
9-
url = git@github.com:Hub-of-all-Things/marketsquare-client-scala-play.git
104
[submodule "frontend"]
115
path = frontend
126
url = git@github.com:Hub-of-all-Things/Rumpel.git

.travis.yml

+17-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
language: scala
22

3+
dist: trusty
4+
35
scala:
46
- 2.11.8
57
jdk:
@@ -12,27 +14,32 @@ branches:
1214
- dev
1315

1416
addons:
15-
postgresql: "9.4"
17+
postgresql: "9.5"
1618

1719
cache:
1820
directories:
1921
- $HOME/.sbt
2022
- $HOME/.ivy2
2123

24+
# Handle git submodules yourself
25+
git:
26+
submodules: false
27+
2228
before_script:
23-
- cd ./hat-database-schema
24-
- source ./env.sh
25-
- bash ./setupDatabase.sh
26-
- bash ./setupAccess.sh
27-
- bash ./applyEvolutions.sh -c structuresonly,testdata
28-
- cd ../
29-
- sed -e "s;%DATABASE%;$DATABASE;g" -e "s;%DBUSER%;$DBUSER;g" -e "s;%DBPASS%;$DBPASS;g" deployment/database.conf.template > src/main/resources/database.conf
30-
- cp ./src/main/resources/database.conf ./codegen/src/main/resources/database.conf
29+
- psql -c "CREATE DATABASE testhatdb1;" -U postgres
30+
- psql -c "CREATE USER testhatdb1 WITH PASSWORD 'testing';" -U postgres
31+
- psql -c "GRANT CREATE ON DATABASE testhatdb1 TO testhatdb1" -U postgres
32+
33+
# Use sed to replace the SSH URL with the public URL, then initialize submodules
34+
before_install:
35+
- sed -i 's/git@github.com:/https:\/\/github.com\//' .gitmodules
36+
- git submodule update --init --recursive
37+
3138

3239
script:
3340
- sbt clean
3441
- sbt ++$TRAVIS_SCALA_VERSION compile test:compile
35-
- sbt coverage "testOnly hatdex.hat.api.*" -Dconfig.file=src/main/resources/application.test.conf
42+
- sbt coverage "testOnly org.hatdex.hat.api.*" -Dconfig.file=src/main/resources/application.test.conf
3643

3744
after_success:
3845
- sbt coveralls

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ To launch the HAT, follow these steps:
7979
```
8080
4. Run the project:
8181
```
82-
sbt "project hat" run
82+
sbt "project hat" -Dconfig.resource=dev.conf run
8383
```
8484
5. Go to http://bobtheplumber.hat.org:9000
8585

codegen/build.sbt

-22
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,6 @@ libraryDependencies ++= Seq(
1010
Library.Slick.slickPgPlayJson,
1111
Library.Slick.slickCodegen,
1212
Library.Akka.slf4j,
13-
Library.Akka.httpCore,
14-
Library.Akka.akkaStream,
15-
Library.Akka.akkaHttpSprayJson,
16-
Library.Utils.jodaTime,
17-
Library.Utils.jodaConvert,
18-
Library.Utils.jts,
19-
Library.Utils.slf4j,
20-
Library.Utils.logbackCore,
21-
Library.Utils.logbackClassic,
22-
23-
Library.Slick.slickPgCore,
24-
Library.Slick.slickPg,
25-
Library.Slick.slickPgJoda,
26-
Library.Slick.slickPgJts,
27-
Library.Slick.slickPgSprayJson,
28-
Library.Slick.slickCodegen,
29-
Library.Akka.slf4j,
30-
Library.Akka.httpCore,
31-
Library.Akka.akkaStream,
32-
Library.Akka.akkaHttpSprayJson,
33-
Library.Akka.akkaActor,
34-
Library.Akka.akkaTestkit,
3513
Library.Utils.jodaTime,
3614
Library.Utils.jodaConvert,
3715
Library.Utils.jts,

codegen/src/main/scala/org/hatdex/hat/dal/SlickPostgresDriver.scala

+36-1
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@
2424

2525
package org.hatdex.hat.dal
2626

27+
import java.sql.Timestamp
28+
2729
import com.github.tminglei.slickpg._
28-
import play.api.libs.json.{ JsValue, Json }
30+
import org.joda.time.DateTime
31+
import play.api.libs.json.{ JsNull, JsValue, Json }
32+
import slick.jdbc.JdbcType
2933

3034
trait SlickPostgresDriver extends ExPostgresDriver
3135
with PgArraySupport
@@ -38,6 +42,7 @@ trait SlickPostgresDriver extends ExPostgresDriver
3842

3943
override val pgjson = "jsonb"
4044
override val api = MyAPI
45+
override protected lazy val useTransactionForUpsert = false
4146

4247
object MyAPI extends API with ArrayImplicits
4348
with DateTimeImplicits
@@ -53,6 +58,36 @@ trait SlickPostgresDriver extends ExPostgresDriver
5358
pgjson,
5459
(s) => utils.SimpleArrayUtils.fromString[JsValue](Json.parse(_))(s).orNull,
5560
(v) => utils.SimpleArrayUtils.mkString[JsValue](_.toString())(v)).to(_.toList)
61+
62+
import scala.language.implicitConversions
63+
64+
override implicit val playJsonTypeMapper: JdbcType[JsValue] =
65+
new GenericJdbcType[JsValue](
66+
pgjson,
67+
(v) => Json.parse(v),
68+
(v) => Json.stringify(v),
69+
zero = JsNull,
70+
hasLiteralForm = false)
71+
72+
override implicit def playJsonColumnExtensionMethods(c: Rep[JsValue]) = {
73+
new FixedJsonColumnExtensionMethods[JsValue, JsValue](c)
74+
}
75+
override implicit def playJsonOptionColumnExtensionMethods(c: Rep[Option[JsValue]]) = {
76+
new FixedJsonColumnExtensionMethods[JsValue, Option[JsValue]](c)
77+
}
78+
79+
class FixedJsonColumnExtensionMethods[JSONType, P1](override val c: Rep[P1])(
80+
implicit
81+
tm: JdbcType[JSONType]) extends JsonColumnExtensionMethods[JSONType, P1](c) {
82+
override def <@:[P2, R](c2: Rep[P2])(implicit om: o#arg[JSONType, P2]#to[Boolean, R]) = {
83+
om.column(jsonLib.ContainsBy, n, c2.toNode)
84+
}
85+
}
86+
87+
val toJson: Rep[String] => Rep[JsValue] = SimpleFunction.unary[String, JsValue]("to_jsonb")
88+
val toTimestamp: Rep[Double] => Rep[Timestamp] = SimpleFunction.unary[Double, Timestamp]("to_timestamp")
89+
val datePart: (Rep[String], Rep[DateTime]) => Rep[String] = SimpleFunction.binary[String, DateTime, String]("date_part")
90+
val datePartTimestamp: (Rep[String], Rep[Timestamp]) => Rep[String] = SimpleFunction.binary[String, Timestamp, String]("date_part")
5691
}
5792

5893
}

deployment/docker/hat/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM java:jre
1+
FROM openjdk:8-jre
22
WORKDIR /opt/docker
33
ADD opt /opt
44
RUN ["chown", "-R", "daemon:daemon", "."]

deployment/docker/hat/docker-build.sh

+5-4
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ set -e
66
HAT_HOME=${HAT_HOME:-"$PWD/hat"} #if executing from deployment/ : "$PWD/../.."
77
DOCKER=${DOCKER:-"$PWD/deployment/docker/hat"}
88
DOCKER_DEPLOY=$DOCKER/docker-deploy
9+
APP=${APPLICATION_NAME:-hat-experimental}
910

1011
echo "Creating $DOCKER_DEPLOY"
1112
mkdir $DOCKER_DEPLOY
1213

13-
echo "Building HAT : sbt docker:stage"
14-
sbt docker:stage
14+
echo "Building ${APP} : sbt docker:stage"
15+
sbt "project hat" docker:stage
1516

1617
if [ ! -f "$HAT_HOME/target/docker/Dockerfile" ]; then
1718
echo "Missing $HAT_HOME/target/docker/Dockerfile"
@@ -24,8 +25,8 @@ cp -r $HAT_HOME/target/docker/stage/opt $DOCKER_DEPLOY/
2425

2526
cp $DOCKER/Dockerfile $DOCKER_DEPLOY/Dockerfile
2627

27-
echo "Building hat docker image: docker-hat"
28-
docker build -t hubofallthings/hat-experimental $DOCKER_DEPLOY
28+
echo "Building hat docker image: docker-hat hubofallthings/${APP}"
29+
docker build -t hubofallthings/${APP} $DOCKER_DEPLOY
2930

3031
echo "Cleaning up"
3132
rm -r $DOCKER_DEPLOY

deployment/ecs/docker-aws-deploy.sh

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
VERSION=`git log --format="%H" -n 1`
6+
7+
echo "Create package"
8+
export APPLICATION_NAME="hat"
9+
HAT_HOME=${PWD} #if executing from deployment/ : "$PWD/../.."
10+
DOCKER=${DOCKER:-"${HAT_HOME}/deployment/docker"}
11+
bash ${DOCKER}/hat/docker-build.sh
12+
13+
echo "Publish to AWS"
14+
docker tag hubofallthings/${APPLICATION_NAME}:latest 717711705314.dkr.ecr.eu-west-1.amazonaws.com/hubofallthings:${VERSION}
15+
docker push 717711705314.dkr.ecr.eu-west-1.amazonaws.com/hubofallthings:${VERSION}
16+
17+
echo "Built application version ${APPLICATION_NAME} ${VERSION}"

frontend

hat-client-scala-play

-1
This file was deleted.

hat/app/org/hatdex/hat/api/controllers/Authentication.scala

+66-9
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,19 @@
2424

2525
package org.hatdex.hat.api.controllers
2626

27+
import java.net.URLDecoder
2728
import javax.inject.Inject
2829

2930
import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository
3031
import com.mohiva.play.silhouette.api.util.{ Clock, Credentials, PasswordHasherRegistry }
3132
import com.mohiva.play.silhouette.api.{ LoginEvent, Silhouette }
32-
import com.mohiva.play.silhouette.impl.exceptions.InvalidPasswordException
33+
import com.mohiva.play.silhouette.impl.exceptions.{ IdentityNotFoundException, InvalidPasswordException }
3334
import com.mohiva.play.silhouette.impl.providers.CredentialsProvider
3435
import org.hatdex.hat.api.json.HatJsonFormats
35-
import org.hatdex.hat.api.models.{ ErrorMessage, SuccessResponse }
36-
import org.hatdex.hat.api.service.UsersService
36+
import org.hatdex.hat.api.models._
37+
import org.hatdex.hat.api.service.{ HatServicesService, MailTokenService, UsersService }
3738
import org.hatdex.hat.authentication._
3839
import org.hatdex.hat.phata.models.{ ApiPasswordChange, ApiPasswordResetRequest, MailTokenUser }
39-
import org.hatdex.hat.phata.service.{ HatServicesService, MailTokenService }
4040
import org.hatdex.hat.resourceManagement.{ HatServerProvider, _ }
4141
import org.hatdex.hat.utils.{ HatBodyParsers, HatMailer }
4242
import play.api.i18n.MessagesApi
@@ -64,7 +64,16 @@ class Authentication @Inject() (
6464

6565
private val logger = Logger(this.getClass)
6666

67-
def hatLogin(name: String, redirectUrl: String): Action[AnyContent] = SecuredAction(WithRole("owner")).async { implicit request =>
67+
def publicKey(): Action[AnyContent] = UserAwareAction.async { implicit request =>
68+
val publicKey = hatServerProvider.toString(request.dynamicEnvironment.publicKey)
69+
Future.successful(Ok(publicKey))
70+
}
71+
72+
def validateToken(): Action[AnyContent] = SecuredAction.async { implicit request =>
73+
Future.successful(Ok(Json.toJson(SuccessResponse("Authenticated"))))
74+
}
75+
76+
def hatLogin(name: String, redirectUrl: String): Action[AnyContent] = SecuredAction(WithRole(Owner())).async { implicit request =>
6877
for {
6978
service <- hatServicesService.findOrCreateHatService(name, redirectUrl)
7079
linkedService <- hatServicesService.hatServiceLink(request.identity, service, Some(redirectUrl))
@@ -74,7 +83,55 @@ class Authentication @Inject() (
7483
}
7584
}
7685

77-
def passwordChangeProcess: Action[ApiPasswordChange] = SecuredAction(WithRole("owner")).async(parsers.json[ApiPasswordChange]) { implicit request =>
86+
def applicationToken(name: String, resource: String): Action[AnyContent] = SecuredAction(WithRole(Owner())).async { implicit request =>
87+
for {
88+
service <- hatServicesService.findOrCreateHatService(name, resource)
89+
token <- hatServicesService.hatServiceToken(request.identity, service)
90+
result <- env.authenticatorService.embed(token.accessToken, Ok(Json.toJson(token)))
91+
} yield {
92+
result
93+
}
94+
}
95+
96+
private val hatService = HatService(
97+
"hat", "hat", "HAT API",
98+
"", "", "",
99+
browser = true,
100+
category = "api",
101+
setup = true,
102+
loginAvailable = true)
103+
def accessToken(): Action[AnyContent] = UserAwareAction.async { implicit request =>
104+
val eventuallyAuthenticatedUser = for {
105+
usernameParam <- request.getQueryString("username").orElse(request.headers.get("username"))
106+
passwordParam <- request.getQueryString("password").orElse(request.headers.get("password"))
107+
} yield {
108+
val username = usernameParam
109+
val password = URLDecoder.decode(passwordParam, "UTF-8")
110+
logger.info(s"Authenticating $username:$password")
111+
credentialsProvider.authenticate(Credentials(username, password))
112+
.flatMap { loginInfo =>
113+
usersService.getUser(loginInfo.providerKey).flatMap {
114+
case Some(user) =>
115+
val customClaims = hatServicesService.generateUserTokenClaims(user, hatService)
116+
for {
117+
authenticator <- env.authenticatorService.create(loginInfo)
118+
token <- env.authenticatorService.init(authenticator.copy(customClaims = Some(customClaims)))
119+
_ <- usersService.logLogin(user, "api", user.roles.filter(_.extra.isEmpty).map(_.title).mkString(":"), None, None)
120+
result <- env.authenticatorService.embed(token, Ok(Json.toJson(AccessToken(token, user.userId))))
121+
} yield {
122+
env.eventBus.publish(LoginEvent(user, request))
123+
result
124+
}
125+
case None => Future.failed(new IdentityNotFoundException("Couldn't find user"))
126+
}
127+
}
128+
}
129+
eventuallyAuthenticatedUser getOrElse {
130+
Future.successful(Unauthorized(Json.toJson(ErrorMessage("Credentials required", "No username or password provided to retrieve token"))))
131+
}
132+
}
133+
134+
def passwordChangeProcess: Action[ApiPasswordChange] = SecuredAction(WithRole(Owner())).async(parsers.json[ApiPasswordChange]) { implicit request =>
78135
request.body.password map { oldPassword =>
79136
val eventualResult = for {
80137
_ <- credentialsProvider.authenticate(Credentials(request.identity.email, oldPassword))
@@ -102,11 +159,10 @@ class Authentication @Inject() (
102159
val email = request.body.email
103160
val response = Ok(Json.toJson(SuccessResponse("If the email you have entered is correct, you will shortly receive an email with password reset instructions")))
104161
if (email == request.dynamicEnvironment.ownerEmail) {
105-
usersService.listUsers.map(_.find(_.role == "owner")).flatMap {
162+
usersService.listUsers.map(_.find(_.roles.contains(Owner()))).flatMap {
106163
case Some(user) =>
107164
val token = MailTokenUser(email, isSignUp = false)
108165
tokenService.create(token).map { _ =>
109-
// TODO generate password reset link for frontend to handle
110166
val scheme = if (request.secure) {
111167
"https://"
112168
}
@@ -134,7 +190,7 @@ class Authentication @Inject() (
134190
tokenService.retrieve(tokenId).flatMap {
135191
case Some(token) if !token.isSignUp && !token.isExpired =>
136192
if (token.email == request.dynamicEnvironment.ownerEmail) {
137-
usersService.listUsers.map(_.find(_.role == "owner")).flatMap {
193+
usersService.listUsers.map(_.find(_.roles.contains(Owner()))).flatMap {
138194
case Some(user) =>
139195
for {
140196
_ <- authInfoRepository.update(user.loginInfo, passwordHasherRegistry.current.hash(request.body.newPassword))
@@ -150,6 +206,7 @@ class Authentication @Inject() (
150206
}
151207
}
152208
else {
209+
logger.info(s"Token email: ${token.email}, while owner email is ${request.dynamicEnvironment.ownerEmail}")
153210
Future.successful(Unauthorized(Json.toJson(ErrorMessage("Password reset unauthorized", "Only HAT owner can reset their password"))))
154211
}
155212
case Some(_) =>

0 commit comments

Comments
 (0)