Skip to content

Commit

Permalink
Merge pull request #373 from obsidiansystems/aa-dessicated-cookies
Browse files Browse the repository at this point in the history
 Provide access to cookies pre- and post-hydration
  • Loading branch information
ali-abrar authored Mar 18, 2019
2 parents bfe4b31 + c8656ae commit abc320a
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 57 deletions.
1 change: 1 addition & 0 deletions lib/backend/obelisk-backend.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ library
build-depends: base,
bytestring,
categories,
cookie,
data-default,
dependent-sum,
dependent-sum-template,
Expand Down
46 changes: 37 additions & 9 deletions lib/backend/src/Obelisk/Backend.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Obelisk.Backend
( Backend (..)
-- * Re-exports
Expand Down Expand Up @@ -42,7 +42,8 @@ import Obelisk.Route
import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
import Reflex.Dom
import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, modifyResponse
, rqPathInfo, rqQueryString, setContentType, writeBS, writeText)
, rqPathInfo, rqQueryString, setContentType, writeBS, writeText
, rqCookies, Cookie(..))
import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)

Expand All @@ -59,7 +60,15 @@ data GhcjsApp route = GhcjsApp

-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
--TODO: The frontend should be provided together with the asset paths so that this isn't so easily breakable; that will probably make this function obsolete
serveDefaultObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> Frontend (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
serveDefaultObeliskApp
:: (MonadSnap m, HasCookies m)
=> (R appRoute
-> Text)
-> ([Text]
-> m ())
-> Frontend (R appRoute)
-> R (ObeliskRoute appRoute)
-> m ()
serveDefaultObeliskApp urlEnc serveStaticAsset frontend = serveObeliskApp urlEnc serveStaticAsset frontendApp
where frontendApp = GhcjsApp
{ _ghcjsApp_compiled = defaultFrontendGhcjsAssets
Expand Down Expand Up @@ -111,7 +120,13 @@ getRouteWith e = do
pageName <- getPageName
return $ tryDecode e pageName

serveObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> GhcjsApp (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
serveObeliskApp
:: (MonadSnap m, HasCookies m)
=> (R appRoute -> Text)
-> ([Text] -> m ())
-> GhcjsApp (R appRoute)
-> R (ObeliskRoute appRoute)
-> m ()
serveObeliskApp urlEnc serveStaticAsset frontendApp = \case
ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
Expand Down Expand Up @@ -140,11 +155,16 @@ staticRenderContentType :: ByteString
staticRenderContentType = "text/html; charset=utf-8"

--TODO: Don't assume we're being served at "/"
serveGhcjsApp :: MonadSnap m => (R appRouteComponent -> Text) -> GhcjsApp (R appRouteComponent) -> R (GhcjsAppRoute appRouteComponent) -> m ()
serveGhcjsApp
:: (MonadSnap m, HasCookies m)
=> (R appRouteComponent -> Text)
-> GhcjsApp (R appRouteComponent)
-> R (GhcjsAppRoute appRouteComponent)
-> m ()
serveGhcjsApp urlEnc app = \case
GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
modifyResponse $ setContentType staticRenderContentType
writeBS <=< liftIO $ renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
writeBS <=< renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments

runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
Expand All @@ -163,11 +183,19 @@ mkRouteToUrl validFullEncoder =
let pageNameEncoder' :: Encoder Identity (Either Text) PageName PathQuery = pageNameEncoder
in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (InR $ ObeliskRoute_App k) :/ v


renderGhcjsFrontend :: MonadIO m => (route -> Text) -> route -> Frontend route -> m ByteString
renderGhcjsFrontend
:: (MonadSnap m, HasCookies m)
=> (route -> Text)
-> route
-> Frontend route
-> m ByteString
renderGhcjsFrontend urlEnc route f = do
let ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
liftIO $ renderFrontendHtml urlEnc route
cookies <- askCookies
renderFrontendHtml cookies urlEnc route
(_frontend_head f >> ghcjsPreload)
(_frontend_body f >> ghcjsScript)

instance HasCookies Snap where
askCookies = map (\c -> (cookieName c, cookieValue c)) <$> getsRequest rqCookies
5 changes: 4 additions & 1 deletion lib/frontend/obelisk-frontend.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ library
hs-source-dirs: src
build-depends: base,
bytestring,
cookie,
dependent-sum,
ghcjs-dom,
jsaddle,
Expand All @@ -20,5 +21,7 @@ library
reflex-dom-core,
text,
transformers
exposed-modules: Obelisk.Frontend
exposed-modules:
Obelisk.Frontend
Obelisk.Frontend.Cookie
ghc-options: -Wall
60 changes: 23 additions & 37 deletions lib/frontend/src/Obelisk/Frontend.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,39 +15,29 @@ module Obelisk.Frontend
, Frontend (..)
, runFrontend
, renderFrontendHtml
, module Obelisk.Frontend.Cookie
) where

import Prelude hiding ((.))

import Control.Category
import Control.Concurrent
import Control.Lens
import Control.Monad hiding (sequence, sequence_)
import Control.Monad.Fix
import Control.Monad.IO.Class
import Control.Monad.Primitive
import Control.Monad.Reader
import Control.Monad.Ref
import Control.Monad.Trans.Class
import Data.ByteString (ByteString)
import Data.Dependent.Sum (DSum (..))
import Data.Foldable (for_)
import Data.Functor.Sum
import Data.IORef
import Data.Monoid ((<>))
import Data.Text (Text)
import Data.Foldable (sequence_)
import Data.Traversable (sequence)
import GHCJS.DOM hiding (bracket, catch)
import GHCJS.DOM.Document
import GHCJS.DOM.Node
import qualified GHCJS.DOM.Types as DOM
import Language.Javascript.JSaddle (JSM)
import Obelisk.Frontend.Cookie
import Obelisk.Route.Frontend
import Reflex.Dom.Core
import Reflex.Host.Class
import qualified Reflex.TriggerEvent.Base as TriggerEvent
import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
import Web.Cookie

makePrisms ''Sum

Expand All @@ -70,6 +60,7 @@ type ObeliskWidget js t route m =
, Prerender js t m
, PrebuildAgnostic t route m
, PrebuildAgnostic t route (Client m)
, HasCookies m
-- TODO Remove these. Probably requires a new class to allow executable-configs to work without being inside a `prerender`
, MonadIO m
, MonadIO (Performable m)
Expand All @@ -86,47 +77,42 @@ data Frontend route = Frontend
, _frontend_body :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
}

runFrontend :: forall backendRoute route. Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> Frontend (R route) -> JSM ()
runFrontend
:: forall backendRoute route
. Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
-> Frontend (R route)
-> JSM ()
runFrontend validFullEncoder frontend = do
let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
errorLeft = \case
Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
Right x -> Identity x
runMyRouteViewT
:: ( TriggerEvent t m
, PerformEvent t m
, MonadHold t m
, DOM.MonadJSM m
, DOM.MonadJSM (Performable m)
, MonadFix m
, MonadFix (Performable m)
)
=> Event t ()
-> RoutedT t (R route) (SetRouteT t (R route) (RouteToUrlT (R route) m)) a
-> m a
runMyRouteViewT = runRouteViewT ve
runHydrationWidgetWithHeadAndBody (pure ()) $ \appendHead appendBody -> do
rec switchover <- runMyRouteViewT switchover $ do
(switchover, fire) <- newTriggerEvent
rec switchover <- runRouteViewT ve switchover $ do
(switchover'', fire) <- newTriggerEvent
mapRoutedT (mapSetRouteT (mapRouteToUrlT appendHead)) $ do
_frontend_head frontend
mapRoutedT (mapSetRouteT (mapRouteToUrlT appendBody)) $ do
_frontend_body frontend
switchover' <- lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
performEvent_ $ liftIO (fire ()) <$ switchover'
pure switchover
pure switchover''
pure ()

renderFrontendHtml
:: (t ~ DomTimeline)
=> (r' -> Text)
:: ( t ~ DomTimeline
, MonadIO m
, widget ~ RoutedT t r (SetRouteT t r' (RouteToUrlT r' (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost))))))
)
=> Cookies
-> (r' -> Text)
-> r
-> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
-> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
-> IO ByteString
renderFrontendHtml urlEnc route headWidget bodyWidget = do
-> widget ()
-> widget ()
-> m ByteString
renderFrontendHtml cookies urlEnc route headWidget bodyWidget = do
--TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
html <- fmap snd $ renderStatic $ fmap fst $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
el "html" $ do
el "head" $ do
let baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
Expand Down
99 changes: 99 additions & 0 deletions lib/frontend/src/Obelisk/Frontend/Cookie.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

module Obelisk.Frontend.Cookie where

import Control.Monad.Fix
import Control.Monad.Primitive
import Control.Monad.Ref
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Trans.Reader
import Data.Text.Encoding (encodeUtf8)
import Reflex
import Reflex.Host.Class
import Reflex.Dom.Core
import GHCJS.DOM.Document (getCookie, Document)
import GHCJS.DOM.Types (MonadJSM)
import Web.Cookie

import Obelisk.Route.Frontend

class Monad m => HasCookies m where
askCookies :: m Cookies
default askCookies :: (HasCookies m', m ~ t m', MonadTrans t) => m Cookies
askCookies = lift askCookies

instance HasCookies m => HasCookies (BehaviorWriterT t w m)
instance HasCookies m => HasCookies (DynamicWriterT t w m)
instance HasCookies m => HasCookies (EventWriterT t w m)
instance HasCookies m => HasCookies (PostBuildT t m)
instance HasCookies m => HasCookies (QueryT t q m)
instance HasCookies m => HasCookies (ReaderT r m)
instance HasCookies m => HasCookies (RequesterT t request response m)
instance HasCookies m => HasCookies (RouteToUrlT t m)
instance HasCookies m => HasCookies (SetRouteT t r m)
instance HasCookies m => HasCookies (StaticDomBuilderT t m)
instance HasCookies m => HasCookies (TriggerEventT t m)
instance HasCookies m => HasCookies (RoutedT t r m)

newtype CookiesT m a = CookiesT { unCookiesT :: ReaderT Cookies m a }
deriving
( Functor
, Applicative
, DomBuilder t
, Monad
, MonadFix
, MonadHold t
, MonadIO
#ifndef ghcjs_HOST_OS
, MonadJSM
#endif
, MonadRef
, MonadReflexCreateTrigger t
, MonadSample t
, MonadTrans
, NotReady t
, PerformEvent t
, PostBuild t
, Prerender js t
, TriggerEvent t
, HasDocument
)

instance Adjustable t m => Adjustable t (CookiesT m) where
runWithReplace a e = CookiesT $ runWithReplace (unCookiesT a) (unCookiesT <$> e)
traverseDMapWithKeyWithAdjust f m e = CookiesT $ traverseDMapWithKeyWithAdjust (\k v -> unCookiesT $ f k v) m e
traverseIntMapWithKeyWithAdjust f m e = CookiesT $ traverseIntMapWithKeyWithAdjust (\k v -> unCookiesT $ f k v) m e
traverseDMapWithKeyWithAdjustWithMove f m e = CookiesT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unCookiesT $ f k v) m e

instance PrimMonad m => PrimMonad (CookiesT m) where
type PrimState (CookiesT m) = PrimState m
primitive = lift . primitive

runCookiesT
:: Cookies
-> CookiesT m a
-> m a
runCookiesT cs child = runReaderT (unCookiesT child) cs

instance Monad m => HasCookies (CookiesT m) where
askCookies = CookiesT ask

mapCookiesT
:: (forall x. m x -> n x)
-> CookiesT m a
-> CookiesT n a
mapCookiesT f (CookiesT x) = CookiesT $ mapReaderT f x

instance (MonadJSM m, RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document) => HasCookies (HydrationDomBuilderT s t m) where
askCookies = fmap (parseCookies . encodeUtf8) $ getCookie =<< askDocument
7 changes: 7 additions & 0 deletions lib/route/src/Obelisk/Route/Frontend.hs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,10 @@ instance Requester t m => Requester t (SetRouteT t r m) where
requesting = SetRouteT . requesting
requesting_ = SetRouteT . requesting_

instance (Monad m, SetRoute t r m) => SetRoute t r (RequesterT t req rsp m) where
setRoute = lift . setRoute
modifyRoute = lift . modifyRoute

#ifndef ghcjs_HOST_OS
deriving instance MonadJSM m => MonadJSM (SetRouteT t r m)
#endif
Expand Down Expand Up @@ -358,6 +362,9 @@ instance (Monad m, RouteToUrl r m) => RouteToUrl r (SetRouteT t r' m) where
instance (Monad m, RouteToUrl r m) => RouteToUrl r (RoutedT t r' m) where
askRouteToUrl = lift askRouteToUrl

instance (Monad m, RouteToUrl r m) => RouteToUrl r (RequesterT t req rsp m) where
askRouteToUrl = lift askRouteToUrl

instance HasJSContext m => HasJSContext (RouteToUrlT r m) where
type JSContextPhantom (RouteToUrlT r m) = JSContextPhantom m
askJSContext = lift askJSContext
Expand Down
1 change: 1 addition & 0 deletions lib/run/obelisk-run.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ library
attoparsec
, base
, bytestring
, cookie
, dependent-sum
, dependent-sum-template
, ghcjs-dom
Expand Down
Loading

0 comments on commit abc320a

Please sign in to comment.