hoauth2-1.7.0: example/App.hs
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
module App (app, waiApp) where
import Control.Monad
import Control.Monad.Error.Class
import Control.Monad.IO.Class (liftIO)
import Data.Bifunctor
import Data.Maybe
import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as TL
import Network.HTTP.Conduit
import Network.HTTP.Types
import Network.OAuth.OAuth2
import qualified Network.Wai as WAI
import Network.Wai.Handler.Warp (run)
import Network.Wai.Middleware.Static
import Prelude
import Web.Scotty
import Web.Scotty.Internal.Types
import IDP
import Session
import Types
import Utils
import Views
------------------------------
-- App
------------------------------
myServerPort :: Int
myServerPort = 9988
app :: IO ()
app = putStrLn ("Starting Server. http://localhost:" ++ show myServerPort)
>> waiApp
>>= run myServerPort
-- TODO: how to add either Monad or a middleware to do session?
waiApp :: IO WAI.Application
waiApp = do
cache <- initCacheStore
initIdps cache
scottyApp $ do
middleware $ staticPolicy (addBase "example/assets")
defaultHandler globalErrorHandler
get "/" $ indexH cache
get "/oauth2/callback" $ callbackH cache
get "/logout" $ logoutH cache
debug :: Bool
debug = True
--------------------------------------------------
-- * Handlers
--------------------------------------------------
redirectToHomeM :: ActionM ()
redirectToHomeM = redirect "/"
errorM :: Text -> ActionM ()
errorM = throwError . ActionError
globalErrorHandler :: Text -> ActionM ()
globalErrorHandler t = status status401 >> html t
logoutH :: CacheStore -> ActionM ()
logoutH c = do
pas <- params
let idpP = paramValue "idp" pas
when (null idpP) redirectToHomeM
let eitherIdpApp = parseIDP (head idpP)
case eitherIdpApp of
Right (IDPApp idp) -> liftIO (removeKey c (idpLabel idp)) >> redirectToHomeM
Left e -> errorM ("logout: unknown IDP " `TL.append` e)
indexH :: CacheStore -> ActionM ()
indexH c = liftIO (allValues c) >>= overviewTpl
callbackH :: CacheStore -> ActionM ()
callbackH c = do
pas <- params
let codeP = paramValue "code" pas
let stateP = paramValue "state" pas
when (null codeP) (errorM "callbackH: no code from callback request")
when (null stateP) (errorM "callbackH: no state from callback request")
let eitherIdpApp = parseIDP (TL.takeWhile (/= '.') (head stateP))
-- TODO: looks like `state` shall be passed when fetching access token
-- turns out no IDP enforce this yet
case eitherIdpApp of
Right (IDPApp idp) -> fetchTokenAndUser c (head codeP) idp
Left e -> errorM ("callbackH: cannot find IDP name from text " `TL.append` e)
fetchTokenAndUser :: (HasTokenReq a, HasUserReq a, HasLabel a)
=> CacheStore
-> TL.Text -- ^ code
-> a
-> ActionM ()
fetchTokenAndUser c code idp = do
maybeIdpData <- lookIdp c idp
when (isNothing maybeIdpData) (errorM "fetchTokenAndUser: cannot find idp data from cache")
let idpData = fromJust maybeIdpData
result <- liftIO $ tryFetchUser idp code
case result of
Right luser -> (updateIdp c idpData luser) >> redirectToHomeM
Left err -> errorM ("fetchTokenAndUser: " `TL.append` err)
where lookIdp c1 idp1 = liftIO $ lookupKey c1 (idpLabel idp1)
updateIdp c1 oldIdpData luser = liftIO $ insertIDPData c1 (oldIdpData {loginUser = Just luser })
-- TODO: may use Exception monad to capture error in this IO monad
--
tryFetchUser :: (HasTokenReq a, HasUserReq a, HasLabel a)
=> a
-> TL.Text -- ^ code
-> IO (Either Text LoginUser)
tryFetchUser idp code = do
mgr <- newManager tlsManagerSettings
token <- tokenReq idp mgr (ExchangeToken $ TL.toStrict code)
when debug (print token)
case token of
Right at -> fetchUser idp mgr (accessToken at)
Left e -> return (Left $ TL.pack $ "tryFetchUser: cannot fetch asses token. error detail: " ++ show e)
-- * Fetch UserInfo
--
fetchUser :: (HasUserReq a) => a -> Manager -> AccessToken -> IO (Either Text LoginUser)
fetchUser idp mgr token = do
re <- userReq idp mgr token
return (first displayOAuth2Error re)
displayOAuth2Error :: OAuth2Error Errors -> Text
displayOAuth2Error = TL.pack . show