{-# 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: " `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)