From 3f0dd35b2b744fea84d1b22025f22a0051f6e174 Mon Sep 17 00:00:00 2001 From: James Morris Date: Mon, 2 Apr 2018 17:57:51 +0100 Subject: [PATCH] Initial Auth0 Integration (#15) * Basic authentication & callback routing * Backend auth dance * Provide an empty history for new users --- README.md | 2 +- bin/gcloud-images.sh | 0 dokusho-server/README.md | 2 +- dokusho-server/build.sbt | 5 +- dokusho-server/src/main/scala/Main.scala | 13 ++- .../scala/dokusho/ReadingHistoryRouter.scala | 77 ++++++++++----- .../scala/dokusho/ReadingHistoryService.scala | 12 +++ .../dokusho/middleware/Auth0Middleware.scala | 52 ++++++++++ dokusho/package.json | 5 +- dokusho/src/App.re | 49 ++++++++-- dokusho/src/app/Actions.re | 7 +- dokusho/src/app/Client.re | 50 +++++++--- dokusho/src/app/Dokusho.re | 3 +- dokusho/src/app/LoginButton.re | 83 ++++++++++++++++ dokusho/src/app/PageType.re | 70 +++++++------- dokusho/src/app/Types.re | 25 +++++ dokusho/src/index.re | 1 + dokusho/src/test/Entry_test.re | 1 - dokusho/yarn.lock | 96 ++++++++++++++++--- 19 files changed, 443 insertions(+), 110 deletions(-) mode change 100644 => 100755 bin/gcloud-images.sh create mode 100644 dokusho-server/src/main/scala/dokusho/middleware/Auth0Middleware.scala create mode 100644 dokusho/src/app/LoginButton.re diff --git a/README.md b/README.md index f327783..a71559a 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/bin/gcloud-images.sh b/bin/gcloud-images.sh old mode 100644 new mode 100755 diff --git a/dokusho-server/README.md b/dokusho-server/README.md index d5968c4..d89ff68 100644 --- a/dokusho-server/README.md +++ b/dokusho-server/README.md @@ -19,7 +19,7 @@ Once the frontend reaches a reasonable level of functionality, this backend serv * Replace a user's reading history * PUT `user/` * Add a new entry to the user's reading history - * POST `user//add` + * POST `user//add` ## Additional Information diff --git a/dokusho-server/build.sbt b/dokusho-server/build.sbt index 2f6cd4f..14db7b7 100644 --- a/dokusho-server/build.sbt +++ b/dokusho-server/build.sbt @@ -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" @@ -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, diff --git a/dokusho-server/src/main/scala/Main.scala b/dokusho-server/src/main/scala/Main.scala index 7fc1219..6ebb5e5 100644 --- a/dokusho-server/src/main/scala/Main.scala +++ b/dokusho-server/src/main/scala/Main.scala @@ -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 @@ -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 } \ No newline at end of file diff --git a/dokusho-server/src/main/scala/dokusho/ReadingHistoryRouter.scala b/dokusho-server/src/main/scala/dokusho/ReadingHistoryRouter.scala index b65cbec..6b60d64 100644 --- a/dokusho-server/src/main/scala/dokusho/ReadingHistoryRouter.scala +++ b/dokusho-server/src/main/scala/dokusho/ReadingHistoryRouter.scala @@ -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) } } diff --git a/dokusho-server/src/main/scala/dokusho/ReadingHistoryService.scala b/dokusho-server/src/main/scala/dokusho/ReadingHistoryService.scala index 6b9f60c..6c85ee6 100644 --- a/dokusho-server/src/main/scala/dokusho/ReadingHistoryService.scala +++ b/dokusho-server/src/main/scala/dokusho/ReadingHistoryService.scala @@ -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)) @@ -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) diff --git a/dokusho-server/src/main/scala/dokusho/middleware/Auth0Middleware.scala b/dokusho-server/src/main/scala/dokusho/middleware/Auth0Middleware.scala new file mode 100644 index 0000000..768e775 --- /dev/null +++ b/dokusho-server/src/main/scala/dokusho/middleware/Auth0Middleware.scala @@ -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 + } + } +} \ No newline at end of file diff --git a/dokusho/package.json b/dokusho/package.json index 06697d1..237b41f 100644 --- a/dokusho/package.json +++ b/dokusho/package.json @@ -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", @@ -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", diff --git a/dokusho/src/App.re b/dokusho/src/App.re index 86876eb..5fe6465 100644 --- a/dokusho/src/App.re +++ b/dokusho/src/App.re @@ -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 =>
- + (switch self.state { + | Routes.Home => + })
-}; \ No newline at end of file +}; diff --git a/dokusho/src/app/Actions.re b/dokusho/src/app/Actions.re index bce55a1..583c54a 100644 --- a/dokusho/src/app/Actions.re +++ b/dokusho/src/app/Actions.re @@ -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 ) ); diff --git a/dokusho/src/app/Client.re b/dokusho/src/app/Client.re index b5d6565..2844022 100644 --- a/dokusho/src/app/Client.re +++ b/dokusho/src/app/Client.re @@ -8,30 +8,51 @@ type serverResponse = { }; module Client = { + open Dom.Storage; + let accessToken = (default) => sessionStorage |> getItem("accessToken") |> Rationale.Option.default(default); let backendURI = "http://35.189.70.144:8080"; let jsonHeader = Fetch.HeadersInit.make({"Content-Type": "application/json"}); + let authHeader = () => Fetch.HeadersInit.makeWithArray([|( "Content-Type", "application/json" ), ( "accessToken", accessToken("accessToken") )|]); + let parseResponse = (json: Js.Json.t) => { Json.Decode.{ userId: json |> field("userId", string), readingHistory: json |> field("readingHistory", Decoders.parseHistory) }; }; - /* Fetches the given user's reading history */ - let userHistory = (userId:string) => + + let parseResponseT = (jString) => + Json.parse(jString) + |> Rationale.Option.default(Json.parseOrRaise("{\"userId\": \"userId\",\"readingHistory\": {\"days\": [] } }")) + |> json => + Json.Decode.{ + userId: json |> field("userId", string), + readingHistory: json |> field("readingHistory", Decoders.parseHistory) + }; + + /* Fetches the given user's reading history, or an empty one */ + let userHistory: string => Js.Promise.t(serverResponse) = (_userId:string) => { + Js.Console.log("Get history: " ++ LoginButton.Auth.getAccessToken()); Js.Promise.( - Fetch.fetch(backendURI ++ "/history/" ++ userId) - |> then_(Fetch.Response.json) - |> then_(resp => resp |> parseResponse |> resolve) + Fetch.fetchWithInit(backendURI ++ "/history", + Fetch.RequestInit.make( + ~method_=Get, + ~headers=authHeader(), + ())) + |> then_(Fetch.Response.text) + |> then_(resp => resp |> parseResponseT |> resolve) ); + }; /* Adds a new reading entry for today to a user's reading history */ - let newEntry = (userId:string, kind: pageType, value: int) => { + let newEntry = (_userId:string, kind: pageType, value: int) => { Js.Promise.( - Fetch.fetchWithInit(backendURI ++ "/history/" ++ userId ++ "/add", - Fetch.RequestInit.make(~method_=Post, + Fetch.fetchWithInit(backendURI ++ "/history/add", + Fetch.RequestInit.make( + ~method_=Post, ~body=Fetch.BodyInit.make(Encoders.endcodeInput(kind, value) |> Js.Json.stringify), - ~headers=jsonHeader, + ~headers=authHeader(), ())) |> then_(Fetch.Response.json) |> then_(resp => resp |> parseResponse |> resolve) @@ -39,12 +60,13 @@ module Client = { }; /* Resets a user's reading history */ - let resetUser = (userId:string) => { + let resetUser = (_userId:string) => { Js.Promise.( - Fetch.fetchWithInit(backendURI ++ "/history/" ++ userId ++ "/reset", - Fetch.RequestInit.make(~method_=Put, - ~headers=jsonHeader, - ())) + Fetch.fetchWithInit(backendURI ++ "/history/reset", + Fetch.RequestInit.make( + ~method_=Put, + ~headers=authHeader(), + ())) |> then_(Fetch.Response.json) |> then_(resp => resp |> parseResponse |> resolve) ); diff --git a/dokusho/src/app/Dokusho.re b/dokusho/src/app/Dokusho.re index 8daa60a..27b83de 100644 --- a/dokusho/src/app/Dokusho.re +++ b/dokusho/src/app/Dokusho.re @@ -8,7 +8,7 @@ open Rationale; module Dokusho { let component = ReasonReact.reducerComponent("Dokusho"); let initState = () => { - readingData: { days : [Day.now()] }, + readingData: { days : [ Day.now() ] }, selectedEntry: Book, selectedDate: Js.Date.make() }; @@ -42,6 +42,7 @@ module Dokusho { let availableDates = DateUtil.availableDates(self.state.readingData);
+
(ReasonReact.stringToElement("Dokusho"))
diff --git a/dokusho/src/app/LoginButton.re b/dokusho/src/app/LoginButton.re new file mode 100644 index 0000000..cf944d5 --- /dev/null +++ b/dokusho/src/app/LoginButton.re @@ -0,0 +1,83 @@ +module Auth { + type generatedAuth0Client = {. + "authorize": [@bs.meth] (unit => unit) + }; + + type clientOptions = { + . + "domain": string, + "clientID": string, + "redirectUri": string, + "responseType": string, + "scope": string + }; + + [@bs.module "auth0-js"] [@bs.new] external createClient : (clientOptions => generatedAuth0Client) = "WebAuth"; + + let matchAccessToken = [%re "/access_token=([^\$&]+)/g"]; + let matchExpiresIn = [%re "/expires_in=([^\$&]+)/g"]; + let matchIdToken = [%re "/id_token=([^\$&]+)/g"]; + + let resolveOption = (opt) => switch opt { + | None => "" + | Some(s) => s + }; + + let resolveRegex = (exp, str) => { + let res = exp |> Js.Re.exec(str); + switch res { + | None => "" + | Some(result) => { + let captures = result |> Js.Re.captures; + switch captures { + | [|_, token|] => token |> Js.Nullable.to_opt |> resolveOption + | _ => "" + }; + } + }; + }; + + open Dom.Storage; + + let handleAuth = (url: ReasonReact.Router.url) => { + + let accessToken = url.hash |> resolveRegex(matchAccessToken); + let idToken = url.hash |> resolveRegex(matchIdToken); + let expiresIn = url.hash |> resolveRegex(matchExpiresIn); + Js.Console.log("Storing auth: " ++ accessToken); + + sessionStorage |> setItem("accessToken", accessToken); + sessionStorage |> setItem("id_token", idToken); + sessionStorage |> setItem("expiresIn", expiresIn); + + accessToken; + }; + + let getIdToken = () => sessionStorage |> getItem("id_token") |> resolveOption; + + let getAccessToken = () => (sessionStorage |> getItem("accessToken") |> resolveOption); +}; + +let authOptions = { + "domain": "dokusho.eu.auth0.com", + "clientID": "mal13rQ1KYJSpAjTBvs72ioa8xhnq8wh", + /* "redirectUri": "http://35.189.106.56:3000/callback", */ + "redirectUri": "http://localhost:3000/callback", + "responseType": "token id_token", + "scope": "openid" +}; + +let authClient = Auth.createClient(authOptions); + +let component = ReasonReact.statelessComponent("LoginButton"); + +let make = (_) => { + ...component, + render: (_) => { + let onLogin = (_event => authClient##authorize()); + + } +}; diff --git a/dokusho/src/app/PageType.re b/dokusho/src/app/PageType.re index 26c805f..e2d3324 100644 --- a/dokusho/src/app/PageType.re +++ b/dokusho/src/app/PageType.re @@ -1,44 +1,40 @@ open Types; -module PageType{ - let pageScore = (pt: pageType) => { - switch pt { - | Manga => 0.2 - | News => 1.0 - | Book => 1.0 - | Lyric => 0.8 - | Net => 1.0 - }; +module PageType = { + let pageScore = (pt: pageType) => + switch pt { + | Manga => 0.2 + | News => 1.0 + | Book => 1.0 + | Lyric => 0.8 + | Net => 1.0 }; - let toString = (pt: pageType) => { - switch pt { - | Manga => "Manga" - | News => "News" - | Book => "Book" - | Lyric => "Lyric" - | Net => "Net" - }; + let toString = (pt: pageType) => + switch pt { + | Manga => "Manga" + | News => "News" + | Book => "Book" + | Lyric => "Lyric" + | Net => "Net" }; - let findOptType: string => option(pageType) = (str) => { - switch str { - | "Manga" => Some(Manga) - | "News" => Some(News) - | "Book" => Some(Book) - | "Lyric" => Some(Lyric) - | "Net" => Some(Net) - | _ => None - }; - }; - + let findOptType: string => option(pageType) = + str => + switch str { + | "Manga" => Some(Manga) + | "News" => Some(News) + | "Book" => Some(Book) + | "Lyric" => Some(Lyric) + | "Net" => Some(Net) + | _ => None + }; - let pageTypes = [ - {name: "Book", pageType: Book}, - {name: "News", pageType: News}, - {name: "Manga", pageType: Manga}, - {name: "Net", pageType: Net}, - {name: "Lyric", pageType: Lyric}]; - - } - \ No newline at end of file + let pageTypes = [ + {name: "Book", pageType: Book}, + {name: "News", pageType: News}, + {name: "Manga", pageType: Manga}, + {name: "Net", pageType: Net}, + {name: "Lyric", pageType: Lyric} + ]; +}; \ No newline at end of file diff --git a/dokusho/src/app/Types.re b/dokusho/src/app/Types.re index 48bac0b..bc6bbe5 100644 --- a/dokusho/src/app/Types.re +++ b/dokusho/src/app/Types.re @@ -1,6 +1,11 @@ /* TODO: Remove this for real authentication */ let testUser = "fully"; +module Routes { + type route = + | Home; +}; + type pageType = | Manga | News @@ -39,7 +44,19 @@ type action = | LoadUserData(string) | SelectDate(Js.Date.t); +type authData = { + accessToken: string, + idToken: string, + expiresIn: string +}; + module Decoders = { + let parseAuthData = (json: Js.Json.t) : authData => + Json.Decode.{ + accessToken: json |> field("accessToken", string), + idToken: json |> field("idToken", string), + expiresIn: json |> field("expiresIn", string) + }; let parsePageType = (asString:string) => { switch (asString) { | "Manga" => Manga @@ -68,6 +85,14 @@ module Decoders = { }; module Encoders = { + let encodeAuthData = authData => + Json.Encode.( + object_([ + ("accessToken", string(authData.accessToken)), + ("idToken", string(authData.idToken)), + ("expiresIn", string(authData.expiresIn)) + ]) + ); let encodeEntry = entry => Json.Encode.( object_([ diff --git a/dokusho/src/index.re b/dokusho/src/index.re index 9d9f182..7a21a8d 100644 --- a/dokusho/src/index.re +++ b/dokusho/src/index.re @@ -4,5 +4,6 @@ [@bs.module "./registerServiceWorker"] external register_service_worker : unit => unit = "default"; ReactDOMRe.renderToElementWithId(, "root"); +ReasonReact.Router.push(""); register_service_worker(); diff --git a/dokusho/src/test/Entry_test.re b/dokusho/src/test/Entry_test.re index 68698ba..112c351 100644 --- a/dokusho/src/test/Entry_test.re +++ b/dokusho/src/test/Entry_test.re @@ -1,6 +1,5 @@ open Jest; -open Entry; open Types; describe("Entry", () => { diff --git a/dokusho/yarn.lock b/dokusho/yarn.lock index fb5f505..1e9dfe9 100644 --- a/dokusho/yarn.lock +++ b/dokusho/yarn.lock @@ -318,6 +318,17 @@ asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" +auth0-js@^9.4.2: + version "9.4.2" + resolved "https://registry.yarnpkg.com/auth0-js/-/auth0-js-9.4.2.tgz#44363933266781fb9447ce09503c6f783b86a474" + dependencies: + base64-js "^1.2.0" + idtoken-verifier "^1.2.0" + qs "^6.4.0" + superagent "^3.8.2" + url-join "^1.1.0" + winchan "^0.2.0" + autoprefixer@7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.6.tgz#fb933039f74af74a83e71225ce78d9fd58ba84d7" @@ -1045,7 +1056,7 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" -base64-js@^1.0.2: +base64-js@^1.0.2, base64-js@^1.2.0: version "1.2.3" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.3.tgz#fb13668233d9614cf5fb4bce95a9ba4096cdf801" @@ -1580,6 +1591,10 @@ commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" +component-emitter@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + compressible@~2.0.13: version "2.0.13" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.13.tgz#0d1020ab924b2fdb4d6279875c7d6daba6baa7a9" @@ -1667,6 +1682,10 @@ cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" +cookiejar@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a" + core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" @@ -1760,6 +1779,10 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" +crypto-js@^3.1.9-1: + version "3.1.9-1" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.1.9-1.tgz#fda19e761fc077e01ffbfdc6e9fdfc59e8806cd8" + crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" @@ -2611,7 +2634,7 @@ express@^4.13.3: utils-merge "1.0.1" vary "~1.1.2" -extend@~3.0.0, extend@~3.0.1: +extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -2815,6 +2838,14 @@ forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" +form-data@^2.3.1, form-data@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + form-data@~2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" @@ -2823,13 +2854,9 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" -form-data@~2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" - dependencies: - asynckit "^0.4.0" - combined-stream "1.0.6" - mime-types "^2.1.12" +formidable@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" forwarded@~0.1.2: version "0.1.2" @@ -3306,6 +3333,16 @@ icss-utils@^2.1.0: dependencies: postcss "^6.0.1" +idtoken-verifier@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/idtoken-verifier/-/idtoken-verifier-1.2.0.tgz#4654f1f07ab7a803fc9b1b8b36057e2a87ad8b09" + dependencies: + base64-js "^1.2.0" + crypto-js "^3.1.9-1" + jsbn "^0.1.0" + superagent "^3.8.2" + url-join "^1.1.0" + ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" @@ -4441,7 +4478,7 @@ js-yaml@~3.7.0: argparse "^1.0.7" esprima "^2.6.0" -jsbn@~0.1.0: +jsbn@^0.1.0, jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -4844,7 +4881,7 @@ merge@^1.1.3: version "1.2.0" resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" -methods@~1.1.2: +methods@^1.1.1, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -6160,7 +6197,7 @@ q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" -qs@6.5.1, qs@~6.5.1: +qs@6.5.1, qs@^6.4.0, qs@^6.5.1, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" @@ -6415,6 +6452,18 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable string_decoder "~1.0.3" util-deprecate "~1.0.1" +readable-stream@^2.0.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + readdirp@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" @@ -7200,6 +7249,21 @@ style-loader@0.19.0: loader-utils "^1.0.2" schema-utils "^0.3.0" +superagent@^3.8.2: + version "3.8.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403" + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.1.1" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.0.5" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -7528,6 +7592,10 @@ urijs@^1.16.1: version "1.19.1" resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a" +url-join@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78" + url-loader@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7" @@ -7833,6 +7901,10 @@ widest-line@^2.0.0: dependencies: string-width "^2.1.1" +winchan@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/winchan/-/winchan-0.2.0.tgz#3863028e7f974b0da1412f28417ba424972abd94" + window-size@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"