{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
import Control.Monad (when)
import Data.Maybe (fromJust, isNothing)
import Data.Serialize (Serialize)
import qualified Data.Text as T (unpack)
import Data.ByteString (ByteString)
import Data.ByteString.Lazy (toStrict, fromStrict)
import Servant (
Proxy(..), Server, (:>), (:<|>)(..)
, Header, Headers, addHeader
, Get, Post, ReqBody, FormUrlEncoded, FromFormUrlEncoded(..))
import Servant.Server (
Context ((:.), EmptyContext)
, serveWithContext)
import Servant.API.Experimental.Auth (AuthProtect)
import Servant.API.ContentTypes (Accept(..), MimeRender(..))
import Servant.Server.Experimental.Auth (AuthHandler)
import Servant.Server.Experimental.Auth.Cookie
import Network.Wai (Application, Request)
import Network.Wai.Handler.Warp (run)
import GHC.Generics
import Network.HTTP.Media ((//), (/:))
import Text.Blaze.Html5 ((!))
import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A
data HTML
instance Accept HTML where
contentType _ = "text" // "html" /: ("charset", "utf-8")
instance MimeRender HTML ByteString where
mimeRender _ x = fromStrict $ x
data Account = Account Int String String
deriving (Show, Eq, Generic)
instance Serialize Account
type instance AuthCookieData = Account
data LoginForm = LoginForm {
username :: String
, password :: String
} deriving (Eq, Show)
instance FromFormUrlEncoded LoginForm where
fromFormUrlEncoded d = do
let username' = lookup "username" d
when (isNothing username') $ Left "username field is missing"
let password' = lookup "password" d
when (isNothing username') $ Left "password field is missing"
Right $ LoginForm {
username = T.unpack . fromJust $ username'
, password = T.unpack . fromJust $ password'
}
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 _ _ [] = Nothing
userLookup username' password' ((Account uid username'' password''):as) =
case (username', password') == (username'', password'') of
True -> Just uid
False -> userLookup username' password' as
type ExampleAPI =
Get '[HTML] ByteString
:<|> "login" :> Get '[HTML] ByteString
:<|> "login" :> ReqBody '[FormUrlEncoded] LoginForm
:> Post '[HTML] (Headers '[Header "set-cookie" ByteString] ByteString)
:<|> "private" :> AuthProtect "cookie-auth" :> Get '[HTML] ByteString
server :: Server ExampleAPI
server = serveHome
:<|> serveLogin
:<|> serveLoginPost
:<|> servePrivate where
serveHome = return $ render homePage
serveLogin = return $ render $ loginPage True
serveLoginPost form = case userLookup (username form) (password form) usersDB of
Nothing -> return $ addHeader "" (render $ loginPage False)
Just uid' -> addSession
authSettings
(Account uid' (username form) (password form))
(render $ redirectPage "/private")
servePrivate (Account uid u p) = return $ render (privatePage uid u p)
render = toStrict . renderHtml
authSettings :: Settings
authSettings = defaultSettings {
cookieFlags = []
, hideReason = False
}
app :: Application
app = serveWithContext
(Proxy :: Proxy ExampleAPI)
((defaultAuthHandler authSettings :: AuthHandler Request Account) :. EmptyContext)
server
main :: IO ()
main = run 8080 app
pageMenu :: H.Html
pageMenu = do
H.a ! A.href "/" $ "home"
_ <- " "
H.a ! A.href "/login" $ "login"
_ <- " "
H.a ! A.href "/private" $ "private"
H.hr
homePage :: H.Html
homePage = H.docTypeHtml $ do
H.head $ do
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 -> H.Html
loginPage firstTime = H.docTypeHtml $ do
H.head $ do
H.title "login"
H.body $ do
pageMenu
H.form ! A.method "post" ! A.action "/login" $ do
H.input ! A.type_ "text" ! A.name "username" >> H.br
H.input ! A.type_ "password" ! A.name "password" >> H.br
H.input ! A.type_ "submit"
when (not firstTime) $ H.p "Incorrect username/password"
privatePage :: Int -> String -> String -> H.Html
privatePage uid username' password' = H.docTypeHtml $ do
H.head $ do
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 -> H.Html
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
"If your browser does not refresh the page click "
H.a ! A.href (H.toValue uri) $ "here"