packages feed

hoauth2-1.16.1: 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.IO.Class         ( MonadIO
                                                , liftIO
                                                )
import           Data.Bifunctor
import           Data.Maybe
import           Data.Text.Lazy                 ( Text )
import qualified Data.Text.Lazy                as TL
import           IDP
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           Session
import           Types
import           Utils
import           Views
import           Web.Scotty

------------------------------
-- 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
    get "/refresh" $ refreshH cache

debug :: Bool
debug = True

--------------------------------------------------
-- * Handlers
--------------------------------------------------

redirectToHomeM :: ActionM ()
redirectToHomeM = redirect "/"

globalErrorHandler :: Text -> ActionM ()
globalErrorHandler t = status status401 >> html t

readIdpParam :: ActionM (Either Text IDPApp)
readIdpParam = do
  pas <- params
  let idpP = paramValue "idp" pas
  when (null idpP) redirectToHomeM
  return $ parseIDP (head idpP)

refreshH :: CacheStore -> ActionM ()
refreshH c = do
  eitherIdpApp <- readIdpParam
  case eitherIdpApp of
    Right (IDPApp idp) -> do
      maybeIdpData <- lookIdp c idp
      when (isNothing maybeIdpData)
           (raise "refreshH: cannot find idp data from cache")
      let idpData = fromJust maybeIdpData
      re <- liftIO $ doRefreshToken idp idpData
      case re of
        Right newToken -> liftIO (print newToken) >> redirectToHomeM -- TODO: update access token in the store
        Left  e        -> raise (TL.pack e)
    Left e -> raise ("logout: unknown IDP " `TL.append` e)

doRefreshToken
  :: HasTokenRefreshReq a => a -> IDPData -> IO (Either String OAuth2Token)
doRefreshToken idp idpData = do
  mgr <- newManager tlsManagerSettings
  case oauth2Token idpData of
    Nothing -> return $ Left "no token found for idp"
    Just at -> case refreshToken at of
      Nothing -> return $ Left "no refresh token presents"
      Just rt -> do
        re <- tokenRefreshReq idp mgr rt
        return (first show re)

logoutH :: CacheStore -> ActionM ()
logoutH c = do
  eitherIdpApp <- readIdpParam
  -- let eitherIdpApp = parseIDP (head idpP)
  case eitherIdpApp of
    Right (IDPApp idp) ->
      liftIO (removeKey c (idpLabel idp)) >> redirectToHomeM
    Left e -> raise ("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)  (raise "callbackH: no code from callback request")
  when (null stateP) (raise "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 ->
      raise ("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)
       (raise "fetchTokenAndUser - cannot find idp data from cache")

  let idpData = fromJust maybeIdpData
  result <- liftIO $ fetchTokenAndUser' c code idp idpData
  case result of
    Right _   -> redirectToHomeM
    Left  err -> raise err

fetchTokenAndUser'
  :: (HasTokenReq a, HasUserReq a)
  => CacheStore
  -> Text
  -> a
  -> IDPData
  -> IO (Either Text ())
fetchTokenAndUser' c code idp idpData = do
  mgr   <- newManager tlsManagerSettings
  token <- tokenReq idp mgr (ExchangeToken $ TL.toStrict code)
  when debug (print token)

  result <- case token of
    Right at -> tryFetchUser mgr at idp
    Left  e  -> return
      (  Left
      $  TL.pack
      $  "tryFetchUser - cannot fetch asses token. error detail: "
      ++ show e
      )

  case result of
    Right (luser, at) -> updateIdp c idpData luser at >> return (Right ())
    Left err ->
      return $ Left ("fetchTokenAndUser - no user found: " `TL.append` err)

 where
  updateIdp c1 oldIdpData luser token = insertIDPData
    c1
    (oldIdpData { loginUser = Just luser, oauth2Token = Just token })

lookIdp :: (MonadIO m, HasLabel a) => CacheStore -> a -> m (Maybe IDPData)
lookIdp c1 idp1 = liftIO $ lookupKey c1 (idpLabel idp1)

-- TODO: may use Exception monad to capture error in this IO monad
--
tryFetchUser
  :: HasUserReq a
  => Manager
  -> OAuth2Token
  -> a
  -> IO (Either Text (LoginUser, OAuth2Token))
tryFetchUser mgr at idp = do
  re <- fetchUser idp mgr (accessToken at)
  return $ case re of
    Right user' -> Right (user', at)
    Left  e     -> Left 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 bslToText re)