{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} module Main (main) where import Prelude () import Prelude.Compat import Control.Monad import Crypto.Random (drgNew) import Data.Default import Data.List (find) import Data.Serialize (Serialize) import GHC.Generics import Network.Wai (Application, Request) import Network.Wai.Handler.Warp (run) #if MIN_VERSION_servant (0,9,0) import Web.FormUrlEncoded (FromForm(..), lookupUnique) #else import Servant (FromFormUrlEncoded(..)) #endif import Servant ((:<|>)(..), (:>), ReqBody, FormUrlEncoded) import Servant (Post, Headers, Header, AuthProtect, Get, Server, Proxy) import Servant (addHeader, serveWithContext, Proxy(..), Context(..)) import Servant.HTML.Blaze import Servant.Server.Experimental.Auth (AuthHandler) import Servant.Server.Experimental.Auth.Cookie import Text.Blaze.Html5 ((!), Markup) import System.Exit (exitSuccess) import System.Environment (getArgs) import qualified Data.Text as T import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A data Account = Account { accUid :: Int , _accUsername :: String , _accPassword :: String } deriving (Show, Eq, Generic) instance Serialize Account type instance AuthCookieData = Account data LoginForm = LoginForm { lfUsername :: String , lfPassword :: String } deriving (Eq, Show) #if MIN_VERSION_servant (0,9,0) instance FromForm LoginForm where fromForm f = do username <- fmap T.unpack $ lookupUnique "username" f password <- fmap T.unpack $ lookupUnique "password" f return LoginForm { lfUsername = username , lfPassword = password } #else instance FromFormUrlEncoded LoginForm where fromFormUrlEncoded d = do username <- case lookup "username" d of Nothing -> Left "username field is missing" Just x -> return (T.unpack x) password <- case lookup "password" d of Nothing -> Left "password field is missing" Just x -> return (T.unpack x) return LoginForm { lfUsername = username , lfPassword = password } #endif usersDB :: [Account] usersDB = [ Account 101 "mr_foo" "password1" , Account 102 "mr_bar" "letmein" , Account 103 "mr_baz" "baseball" ] userLookup :: String -> String -> [Account] -> Maybe Int userLookup username password db = accUid <$> find f db where f (Account _ u p) = u == username && p == password type ExampleAPI = Get '[HTML] Markup :<|> "login" :> Get '[HTML] Markup :<|> "login" :> ReqBody '[FormUrlEncoded] LoginForm :> Post '[HTML] (Headers '[Header "set-cookie" EncryptedSession] Markup) :<|> "private" :> AuthProtect "cookie-auth" :> Get '[HTML] Markup server :: AuthCookieSettings -> RandomSource -> ServerKey -> Server ExampleAPI server settings rs sk = serveHome :<|> serveLogin :<|> serveLoginPost :<|> servePrivate where serveHome = return homePage serveLogin = return (loginPage True) serveLoginPost LoginForm {..} = case userLookup lfUsername lfPassword usersDB of Nothing -> return $ addHeader emptyEncryptedSession (loginPage False) Just uid -> addSession settings -- the settings rs -- random source sk -- server key (Account uid lfUsername lfPassword) (redirectPage "/private") servePrivate (Account uid u p) = return (privatePage uid u p) app :: AuthCookieSettings -> RandomSource -> ServerKey -> Application app settings rs sk = serveWithContext (Proxy :: Proxy ExampleAPI) ((defaultAuthHandler settings sk :: AuthHandler Request Account) :. EmptyContext) (server settings rs sk) main :: IO () main = do args <- getArgs when (args /= ["run"]) $ do putStrLn "Use './example run' to run an example" exitSuccess rs <- mkRandomSource drgNew 1000 sk <- mkServerKey 16 Nothing run 8080 (app def rs sk) pageMenu :: Markup pageMenu = do H.a ! A.href "/" $ "home" void " " H.a ! A.href "/login" $ "login" void " " H.a ! A.href "/private" $ "private" H.hr homePage :: Markup homePage = H.docTypeHtml $ do H.head (H.title "home") H.body $ do pageMenu H.p "This is an example of using servant-auth-cookie library." H.p "Use login page to get access to the private page." loginPage :: Bool -> Markup loginPage firstTime = H.docTypeHtml $ do H.head (H.title "login") H.body $ do pageMenu H.form ! A.method "post" ! A.action "/login" $ do H.table $ do H.tr $ do H.td "username:" H.td (H.input ! A.type_ "text" ! A.name "username") H.tr $ do H.td "password:" H.td (H.input ! A.type_ "password" ! A.name "password") H.input ! A.type_ "submit" unless firstTime $ H.p "Incorrect username/password" privatePage :: Int -> String -> String -> Markup privatePage uid username' password' = H.docTypeHtml $ do H.head (H.title "private") H.body $ do pageMenu H.p $ H.b "ID: " >> H.toHtml (show uid) H.p $ H.b "username: " >> H.toHtml username' H.p $ H.b "password: " >> H.toHtml password' redirectPage :: String -> Markup redirectPage uri = H.docTypeHtml $ do H.head $ do H.title "redirecting..." H.meta ! A.httpEquiv "refresh" ! A.content (H.toValue $ "1; url=" ++ uri) H.body $ do H.p "You are being redirected." H.p $ do void "If your browser does not refresh the page click " H.a ! A.href (H.toValue uri) $ "here"