Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Auth0 Integration #15

Merged
merged 16 commits into from
Apr 2, 2018
Merged
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ The backend's location can then be found using: `minikube service backend --url`
This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app).

* [ReasonML](https://reasonml.github.io/)
* [Reason Scripts](https://github.com/reasonml-community/reason-scripts)
* [Reason React](https://reasonml.github.io/reason-react/)
* [Scala](http://scala-lang.org)
* [Http4s](http://http4s.org)
* [Kubernetes](https://kubernetes.io)
* [Minikube](https://github.com/kubernetes/minikube)
* [MongoDB](https://www.mongodb.com)
* [Auth0](https://www.auth0.com)
Empty file modified bin/gcloud-images.sh
100644 → 100755
Empty file.
2 changes: 1 addition & 1 deletion dokusho-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Once the frontend reaches a reasonable level of functionality, this backend serv
* Replace a user's reading history
* PUT `user/<userID>`
* Add a new entry to the user's reading history
* POST `user/<userID>/add`
* POST `user/<userID>/add`


## Additional Information
Expand Down
5 changes: 3 additions & 2 deletions dokusho-server/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ name := "dokusho-server"
mainClass in(Compile, run) := Some("Main")

val SCALA_VERSION = "2.12.4"
val CIRCE_VERSION = "0.9.1"
val HTTP4S_VERSION = "0.18.1"
val CIRCE_VERSION = "0.9.2"
val HTTP4S_VERSION = "0.18.3"
val MONGO_VERSION = "2.2.1"
val MONOCLE_VERSION = "1.5.0"

Expand All @@ -18,6 +18,7 @@ libraryDependencies ++= Seq(
"org.http4s" %% "http4s-dsl" % HTTP4S_VERSION,
"org.http4s" %% "http4s-blaze-server" % HTTP4S_VERSION,
"org.http4s" %% "http4s-circe" % HTTP4S_VERSION,
"org.http4s" %% "http4s-blaze-client" % HTTP4S_VERSION,

"io.circe" %% "circe-generic" % CIRCE_VERSION,
"io.circe" %% "circe-generic-extras" % CIRCE_VERSION,
Expand Down
13 changes: 11 additions & 2 deletions dokusho-server/src/main/scala/Main.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import cats.effect._
import cats.effect.IO
import dokusho.middleware.Auth0Middleware
import dokusho.{MongoRepository, ReadingHistoryRouter, ReadingHistoryService}
import fs2.StreamApp.ExitCode
import fs2.{Stream, StreamApp}
import org.http4s.client.Client
import org.http4s.client.blaze.PooledHttp1Client
import org.http4s.server.ServerBuilder
import org.http4s.server.blaze.BlazeBuilder

Expand All @@ -15,14 +18,20 @@ object Main extends StreamApp[IO] {
"test",
"dokusho")

lazy val pclient: Client[IO] = PooledHttp1Client[IO]()

val authMiddleware: Auth0Middleware = new Auth0Middleware(pclient)
val readingHistoryService = new ReadingHistoryService(mongo)
val historyService = new ReadingHistoryRouter(readingHistoryService)

val authHistory = authMiddleware.authenticationMiddleware(historyService.routes)


override def stream(args: List[String], requestShutdown: IO[Unit]): Stream[IO, ExitCode] =
BlazeBuilder[IO]
.bindHttp(8080, "0.0.0.0")
.mountService(historyService.routes, "/")
.mountService(historyService.routes, "/noauth")
.mountService(authHistory, "/")
.withBanner(ServerBuilder.DefaultBanner)
.serve
}
77 changes: 51 additions & 26 deletions dokusho-server/src/main/scala/dokusho/ReadingHistoryRouter.scala
Original file line number Diff line number Diff line change
@@ -1,37 +1,62 @@
package dokusho

import cats.data.OptionT
import cats.effect.IO
import io.circe.Json
import org.http4s.HttpService
import org.http4s.util.CaseInsensitiveString
import org.http4s.{Header, HttpService, Request, Response}

class ReadingHistoryRouter(readingHistoryService: ReadingHistoryService) extends Http4sRouter {

private val service = readingHistoryService
case class SuccessfulPut(userId: String)

def getUserId(req: Request[IO]): IO[Option[String]] =
IO(req.headers.find(_.name == CaseInsensitiveString("User")).map(_.value))

implicit class ReqHelper(req: Request[IO]) {
def withReadingHistory(f: ReadingHistory => IO[UserReadingHistory]): IO[UserReadingHistory] =
req.as[ReadingHistory].flatMap(f(_))

def withEntry(f: NewEntry => IO[UserReadingHistory]): IO[UserReadingHistory] =
req.as[NewEntry].flatMap(f(_))
}

implicit private def routeWithErrorHandling(io: OptionT[IO, IO[Response[IO]]]): IO[Response[IO]] =
io.value.flatMap(_.getOrElse(NotFound()))

val orNotFound = (io: OptionT[IO, IO[Response[IO]]]) =>
io.value.flatMap(_.getOrElse(NotFound()))

val routes: HttpService[IO] = HttpService[IO] {
case GET -> Root / "history" / userId =>
for {
userReadingHistory <- readingHistoryService.getReadingHistory(userId)
json: Option[Json] = userReadingHistory.map(_.asJson)
resp <- json.fold(NotFound())(j => Ok(j))
} yield resp
case req@PUT -> Root / "history" / userId =>
for {
readingHistory <- req.as[ReadingHistory]
storedHistory <- readingHistoryService.upsert(UserReadingHistory(userId, readingHistory))
json: Json = SuccessfulPut(storedHistory.userId).asJson
response <- Ok(json)
} yield response
case req@POST -> Root / "history" / userId / "add" =>
for {
entry <- req.as[NewEntry]
storedHistory <- readingHistoryService.addNewEntry(userId, entry)
json = storedHistory.map(_.asJson)
result <- json.fold(NotFound())(j => Ok(j))
} yield result
case PUT -> Root / "history" / userId / "reset" =>
readingHistoryService.reset(userId)
case req@GET -> Root / "history" =>
OptionT(getUserId(req))
.flatMapF(userId => service.getReadingHistory(userId))
.map(_.asJson)
.flatMap(j => Ok(j))
.map(json => Ok(json))
.value.flatMap(_.getOrElse(NotFound()))
case req@PUT -> Root / "history" =>
OptionT(getUserId(req))
.semiflatMap(userId => req.withReadingHistory(rh => service.upsert(UserReadingHistory(userId, rh))))
.map(storedHistory => SuccessfulPut(storedHistory.userId))
.map(sp => Ok(sp.asJson))
.value.flatMap(_.getOrElse(NotFound()))
case req@POST -> Root / "history" / "add" =>
OptionT(getUserId(req))
.semiflatMap(userId => req.withEntry(e => service.upsertNewEntry(userId, e)))
.map(storedHistory => storedHistory.asJson)
.map(json => Ok(json))
.value.flatMap(_.getOrElse(BadRequest()))
case req@PUT -> Root / "history" / "reset" =>
OptionT(getUserId(req))
.semiflatMap(userId => service.reset(userId))
.map(_.asJson)
.map(j => Ok(j))
.value.flatMap(_.getOrElse(BadRequest()))
case req@GET -> Root / "auth" =>
// This endpoint should be removed, but right now it's handy for development
val headerOpt: Header = req.headers
.find(_.name == CaseInsensitiveString("User"))
.getOrElse(Header("User", "None"))

Ok("Hello: " + headerOpt.value)
}
}
12 changes: 12 additions & 0 deletions dokusho-server/src/main/scala/dokusho/ReadingHistoryService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class ReadingHistoryService(mongoRepository: HistoryRepository) {
def getReadingHistory(userId: String): IO[Option[UserReadingHistory]] =
mongoRepository.get(userId)

def getOrMakeReadingHistory(userId: String): IO[UserReadingHistory] =
OptionT(mongoRepository.get(userId))
.getOrElseF(reset(userId))

def addNewEntry(userId: String, newEntry: NewEntry): IO[Option[UserReadingHistory]] = {
lazy val update = daysLens.modify(updateDay(newEntry))
OptionT(getReadingHistory(userId))
Expand All @@ -22,6 +26,14 @@ class ReadingHistoryService(mongoRepository: HistoryRepository) {
.value
}

def upsertNewEntry(userId: String, newEntry: NewEntry): IO[UserReadingHistory] = {
lazy val update = daysLens.modify(updateDay(newEntry))
OptionT(getReadingHistory(userId))
.getOrElseF(reset(userId))
.map(update)
.flatMap(upsert)
}

def upsert(userReadingHistory: UserReadingHistory): IO[UserReadingHistory] =
mongoRepository.put(userReadingHistory)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package dokusho.middleware

import cats.data.OptionT
import cats.effect.IO
import dokusho.Http4sRouter
import org.http4s._
import org.http4s.client.Client
import org.http4s.util.CaseInsensitiveString

sealed trait Auth0Response

case class AuthSuccessResponse(sub: String) extends Auth0Response

case class FailureResponse(reason: String) extends Auth0Response

class Auth0Middleware(client: Client[IO]) extends Http4sRouter {

def authenticationMiddleware(service: HttpService[IO]): HttpService[IO] = cats.data.Kleisli { req: Request[IO] =>

val authToken: Option[Header] =
req.headers
.find(_.name == CaseInsensitiveString("accessToken"))
.map(header => Header.apply("Authorization", "Bearer " + header.value))

authToken match {
case Some(headers) =>
OptionT(
callService(client, headers)
.map(userInfo => req.putHeaders(Header.apply("User", userInfo)))
.flatMap(req => service.apply(req.putHeaders(headers)).value))
case None =>
val response: Response[IO] = Response.apply[IO](status = Unauthorized)
OptionT(IO.pure(Option(response)))
}
}

private def callService(c: Client[IO], authToken: Header): IO[String] = {
c.fetch(Request.apply[IO](
method = GET,
uri = Uri.unsafeFromString("https://dokusho.eu.auth0.com/userinfo"),
headers = Headers(authToken)
)) { r =>
r.status.responseClass match {
case Status.Successful => r.as[AuthSuccessResponse]
case _ => IO.apply(FailureResponse("unauthorised"))
}
}.map {
case AuthSuccessResponse(sub: String) => println(s"Got sub: $sub"); sub.dropWhile(_ != '|').tail
case FailureResponse(r: String) => r
}
}
}
5 changes: 3 additions & 2 deletions dokusho/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@glennsl/bs-json": "^1.1.3",
"auth0-js": "^9.4.2",
"bs-fetch": "^0.2.1",
"isomorphic-fetch": "^2.2.1",
"rationale": "^0.1.3",
Expand All @@ -13,13 +14,13 @@
},
"scripts": {
"start": "react-scripts start",
"build": "react-toolbox-themr react-scripts build",
"build": "react-toolbox-themr && react-scripts build",
"test": "react-scripts test --env=jsdom",
"coverage": "react-scripts test --env=jsdom --coverage",
"ci": "react-scripts test --env=jsdom --coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
"eject": "react-scripts eject",
"prepare": "npm link bs-platform",
"clean": "rm -rf lib && rm -rf node_modules"
"clean": "rm -rf lib && rm -rf node_modules && rm -rf build"
},
"devDependencies": {
"@astrada/reason-react-toolbox": "^0.4.2",
Expand Down
49 changes: 42 additions & 7 deletions dokusho/src/App.re
Original file line number Diff line number Diff line change
@@ -1,19 +1,54 @@
[%bs.raw {|require('./toolbox/theme.css')|}];

open Dokusho;
[%bs.raw {|require('../node_modules/auth0-js/build/auth0.js')|}];

[@bs.module]
external theme : ReactToolbox.ThemeProvider.theme = "./toolbox/theme";
[%bs.raw {|require('./toolbox/theme.css')|}];

let component = ReasonReact.statelessComponent("App");
open Dokusho;
open Types;

type action =
| ChangeRoute(Routes.route);

let reducer = (action, _state) =>
switch action {
| ChangeRoute(route) => ReasonReact.Update( route )
};

let component = ReasonReact.reducerComponent("App");

let mapUrlToRoute = (url: ReasonReact.Router.url) => {
switch url.path {
| ["callback"] => {
let _token = LoginButton.Auth.handleAuth(url);
Routes.Home;
}
| [] => {
Js.Console.log("Home");
Routes.Home;
}
| _ => {
Routes.Home;
} /* Routes.NotFound */
}
};

let make = _children => {
...component,
render: _self =>
reducer,
initialState: () => { Routes.Home },
subscriptions: (self) => [
Sub(
() => ReasonReact.Router.watchUrl((url) => self.send(ChangeRoute(url |> mapUrlToRoute))),
ReasonReact.Router.unwatchUrl
)
],
render: self =>
<ReactToolbox.ThemeProvider theme>
<div className="app">
<Dokusho/>
(switch self.state {
| Routes.Home => <Dokusho/>
})
</div>
</ReactToolbox.ThemeProvider>
};
};
7 changes: 3 additions & 4 deletions dokusho/src/app/Actions.re
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,15 @@ module Actions = {

let loadUserData = (userId) => ReasonReact.SideEffects(
(self =>
Js.Promise.(
Client.userHistory(userId)
|> then_((serverResponse: serverResponse) => {
|> Js.Promise.then_((serverResponse: serverResponse) => {
if(List.length(serverResponse.readingHistory.days) != 0) {
self.send(
UpdateHistory(
serverResponse.readingHistory.days))
};
resolve(serverResponse);
}))
Js.Promise.resolve(serverResponse);
})
|> ignore
)
);
Expand Down
Loading