Route attributes
Route attributes allow you to set some metadata on each of your routes, in the
routes description itself. The syntax is trivial: just an exclamation point
followed by a value. Using it is also trivial: just use the routeAttrs
function.
It’s easiest to understand how it all fits together, and when you might want it, with a motivating example. The case I personally most use this for is annotating administrative routes. Imagine having a website with about 12 different admin actions. You could manually add a call to requireAdmin
or some such at the beginning of each action, but:
-
It’s tedious.
-
It’s error prone: you could easily forget one.
-
Worse yet, it’s not easy to notice that you’ve missed one.
Modifying your isAuthorized
method with an explicit list of administrative
routes is a bit better, but it’s still difficult to see at a glance when you’ve
missed one.
This is why I like to use route attributes for this: you add a single word to
each relevant part of the route definition, and then you just check for that
attribute in isAuthorized
. Let’s see the code!
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import Data.Set (member)
import Data.Text (Text)
import Yesod
import Yesod.Auth
import Yesod.Auth.Dummy
data App = App
mkYesod "App" [parseRoutes|
/ HomeR GET
/unprotected UnprotectedR GET
/admin1 Admin1R GET !admin
/admin2 Admin2R GET !admin
/admin3 Admin3R GET
/auth AuthR Auth getAuth
|]
instance Yesod App where
authRoute _ = Just $ AuthR LoginR
isAuthorized route _writable
| "admin" `member` routeAttrs route = do
muser <- maybeAuthId
case muser of
Nothing -> return AuthenticationRequired
Just ident
-- Just a hack since we're using the dummy module
| ident == "admin" -> return Authorized
| otherwise -> return $ Unauthorized "Admin access only"
| otherwise = return Authorized
instance RenderMessage App FormMessage where
renderMessage _ _ = defaultFormMessage
-- Hacky YesodAuth instance for just the dummy auth plugin
instance YesodAuth App where
type AuthId App = Text
loginDest _ = HomeR
logoutDest _ = HomeR
getAuthId = return . Just . credsIdent
authPlugins _ = [authDummy]
maybeAuthId = lookupSession credsKey
authHttpManager = error "no http manager provided"
getHomeR :: Handler Html
getHomeR = defaultLayout $ do
setTitle "Route attr homepage"
[whamlet|
<p>
<a href=@{UnprotectedR}>Unprotected
<p>
<a href=@{Admin1R}>Admin 1
<p>
<a href=@{Admin2R}>Admin 2
<p>
<a href=@{Admin3R}>Admin 3
|]
getUnprotectedR, getAdmin1R, getAdmin2R, getAdmin3R :: Handler Html
getUnprotectedR = defaultLayout [whamlet|Unprotected|]
getAdmin1R = defaultLayout [whamlet|Admin1|]
getAdmin2R = defaultLayout [whamlet|Admin2|]
getAdmin3R = defaultLayout [whamlet|Admin3|]
main :: IO ()
main = warp 3000 App
And it was so glaring, I bet you even caught the security hole about Admin3R
.