Skip to content

Commit

Permalink
Initial Auth0 Integration (#15)
Browse files Browse the repository at this point in the history
* Basic authentication & callback routing

* Backend auth dance

* Provide an empty history for new users
  • Loading branch information
RawToast committed Apr 2, 2018
1 parent d81a073 commit 3f0dd35
Show file tree
Hide file tree
Showing 19 changed files with 443 additions and 110 deletions.
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

0 comments on commit 3f0dd35

Please sign in to comment.