{-# LANGUAGE Trustworthy #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Provides generic and HTTP Basic authentication.
module LIO.Web.Simple.Auth ( basicAuth
                           , handleAuth
                           , requestLogin
                           -- * Helpers
                           , withUserOrLogin
                           , currentUser
                           ) where

import Data.Maybe
import Control.Monad
import qualified Data.ByteString.Char8 as S8
import Data.ByteString.Base64
import Network.HTTP.Types
import Network.Wai
import Web.Simple.Responses
import Web.Simple.Controller.Trans


-- | A middleware that uses HTTP basic authentication to authenticate
-- a request for a realm with the given username and password. The
-- request is rewritten with an @X-User@ request header containing the
-- authenticated username before being passed to the next
-- 'application'. Note that the HTTP basic authentication header is
-- only set if the executed app requests it, by setting the @X-Login@
-- response header (e.g., with 'requestLogin').
basicAuth :: Monad m
          => String                          -- ^ Realm
          -> (S8.ByteString -> S8.ByteString -> m Bool)
          -> SimpleMiddleware m
basicAuth realm auth app0 req0 = handleAuth authApp (mkApp app0) req0
  where authApp = const . return $ requireBasicAuth realm
        mkApp app req =  case getBasicAuthLogin req of
          Nothing -> app req
          Just (usr, pwd) -> do
            success <- auth usr pwd
            let req' = req { requestHeaders = ("X-User", usr) : 
                                              requestHeaders req }
            if success then app req' else app req


-- | Helper method for implementing basic authentication. Given a
-- 'Request' returns the (username, password) pair from the basic
-- authentication header if present.
getBasicAuthLogin :: Request -> Maybe (S8.ByteString, S8.ByteString)
getBasicAuthLogin req = do
  authStr <- lookup hAuthorization $ requestHeaders req
  unless ("Basic" `S8.isPrefixOf` authStr) $ fail "Not basic auth."
  let up = fmap (S8.split ':') $ decode $ S8.drop 6 authStr
  case up of
     Right (user:pwd:[]) -> return (user, pwd)
     _ -> fail "Malformed basic auth header."

-- | Executes the app and if the app 'Response' has header
-- @X-Login@ and the user is not logged in, i.e., the @X-User@ request
-- header is not present, execute the login application.
handleAuth :: Monad m => SimpleApplication m -> SimpleMiddleware m
handleAuth loginApp app req = do
  resp <- app req
  if hasLogin resp && notLoggedIn
    then loginApp req
    else return resp
  where hasLogin r  = "X-Login" `isIn` responseHeaders r
        notLoggedIn = not $ "X-User" `isIn` requestHeaders req
        isIn n xs   = isJust $ lookup n xs

-- | Request authentication middleware to authenticate user
requestLogin :: Response
requestLogin = responseLBS status400 [("X-Login", "True")] ""

-- | Execute action with the current user's name. Otherwise, request
-- that the user authenticate.
withUserOrLogin :: Monad m 
                => (S8.ByteString -> ControllerT r m a)
                -> ControllerT r m a
withUserOrLogin act = currentUser >>= \muser ->
  maybe (respond requestLogin) act muser

-- | Get the current user.
currentUser :: Monad m => ControllerT r m (Maybe S8.ByteString)
currentUser = requestHeader "X-User"