{-# LANGUAGE
OverloadedStrings
, ScopedTypeVariables
, FlexibleContexts
, DeriveDataTypeable
, DataKinds
#-}
module Main where
import Web.Routes.Nested
import Network.Wai.Trans
import Network.Wai.Handler.Warp
import Network.HTTP.Types
import Text.Regex
import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import Data.Attoparsec.Text hiding (match)
import Data.Monoid
import Data.Typeable
import Control.Monad.IO.Class
import Control.Monad.Catch
import Control.Monad.Trans
defApp :: Application
defApp _ respond = respond
$ textOnly "404 :(" status404 []
main :: IO ()
main = run 3000 $
(routeAuth authorize routes
`catchMiddlewareT` handleUploadError
`catchMiddlewareT` handleAuthError
) defApp
where
handleUploadError u = fileExtsToMiddleware $
overFileExts [Text] (mapStatus $ const status400) $
case u of
NoChunkedBody -> text "No chunked body allowed!"
UploadTooLarge -> text "Upload too large"
handleAuthError e = fileExtsToMiddleware $
overFileExts [Text] (mapStatus $ const status400) $
case e of
NeedsAuth -> text "Authentication needed"
-- | A set of symbolic security roles
data AuthRole = AuthRole
deriving (Show, Eq)
-- | The error thrown during authentication or authorization.
--
-- I would encourage other security-related error schemes:
--
-- - one data type for user authority errors - like when a logged-in user tries to access
-- the admin console
-- - one for types of failed authentication, during login (user doesn't exist,
-- incorrect password, etc.)
-- - one for malicious attacks - for instance if someone has tried (and failed) to login
-- 5 times in the last minute, or multiple requests to the same resource
data AuthError = NeedsAuth
deriving (Show, Eq, Typeable)
instance Exception AuthError
-- | Simple function that returns a response modification function - like
-- something that adds to the response headers - to maintain a session cookie,
-- for instance.
--
-- You can use 'Control.Monad.Catch.throwM' to throw arbitrary errors, but
-- you should also catch them with 'Network.Wai.Trans.catchMiddlewareT' to
-- avoid runtime exceptions.
authorize :: ( Monad m
, MonadThrow m
) => Request -> [AuthRole] -> m ()
authorize req ss | null ss = return ()
| otherwise = throwM NeedsAuth
-- | The error thrown during the uploading function
data UploadError = NoChunkedBody
| UploadTooLarge
deriving (Show, Typeable)
instance Exception UploadError
routes :: ( MonadIO m
, MonadThrow m
) => HandlerT (MiddlewareT m) (SecurityToken AuthRole) m ()
routes = do
matchHere (action rootHandle)
matchGroup (l_ "foo" </> o_) $ do
matchHere (action fooHandle)
auth AuthRole DontProtectHere
match (l_ "bar" </> o_) (action barHandle)
match (p_ "double" double </> o_) doubleHandle
match emailRoute emailHandle
match (l_ "baz" </> o_) (\app req -> action (bazHandle req) app req)
matchAny (action notFoundHandle)
where
rootHandle :: MonadIO m => ActionT m ()
rootHandle =
get $ do
text "Home"
json ("Home" :: T.Text)
-- `/foo`
fooHandle :: MonadIO m => ActionT m ()
fooHandle = get $ text "foo!"
-- `/foo/bar`
barHandle :: MonadIO m => ActionT m ()
barHandle =
get $ do
text "bar!"
json ("json bar!" :: T.Text)
-- `/foo/1234e12`, uses attoparsec
doubleHandle :: MonadIO m => Double-> MiddlewareT m
doubleHandle d = action $ get $ text $ LT.pack (show d) <> " foos"
-- `/athan@foo.com`
emailRoute :: UrlChunks '[ 'Just [String] ]
emailRoute = r_ "email" (mkRegex "(^[-a-zA-Z0-9_.]+@[-a-zA-Z0-9]+\\.[-a-zA-Z0-9.]+$)") </> o_
emailHandle :: MonadIO m => [String] -> MiddlewareT m
emailHandle e = action $ get $ text $ LT.pack (show e) <> " email"
-- `/baz
bazHandle :: ( MonadIO m
, MonadThrow m
) => Request -> ActionT m ()
bazHandle req = do
get $ text "baz!"
post $ do
lift $ uploader req
text "uploaded!"
where
uploader :: (MonadIO m, MonadThrow m) => Request -> m ()
uploader req =
case requestBodyLength req of
ChunkedBody -> throwM NoChunkedBody
KnownLength l | l > 1024 -> throwM UploadTooLarge
| otherwise -> liftIO $ print =<< strictRequestBody req
notFoundHandle :: MonadIO m => ActionT m ()
notFoundHandle = get $
overFileExts [Text] (mapStatus $ const status400) $
text "Not Found :("