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:

  1. It’s tedious.

  2. It’s error prone: you could easily forget one.

  3. 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!

  1. {-# LANGUAGE MultiParamTypeClasses #-}
  2. {-# LANGUAGE OverloadedStrings #-}
  3. {-# LANGUAGE QuasiQuotes #-}
  4. {-# LANGUAGE TemplateHaskell #-}
  5. {-# LANGUAGE TypeFamilies #-}
  6. import Data.Set (member)
  7. import Data.Text (Text)
  8. import Yesod
  9. import Yesod.Auth
  10. import Yesod.Auth.Dummy
  11. data App = App
  12. mkYesod "App" [parseRoutes|
  13. / HomeR GET
  14. /unprotected UnprotectedR GET
  15. /admin1 Admin1R GET !admin
  16. /admin2 Admin2R GET !admin
  17. /admin3 Admin3R GET
  18. /auth AuthR Auth getAuth
  19. |]
  20. instance Yesod App where
  21. authRoute _ = Just $ AuthR LoginR
  22. isAuthorized route _writable
  23. | "admin" `member` routeAttrs route = do
  24. muser <- maybeAuthId
  25. case muser of
  26. Nothing -> return AuthenticationRequired
  27. Just ident
  28. -- Just a hack since we're using the dummy module
  29. | ident == "admin" -> return Authorized
  30. | otherwise -> return $ Unauthorized "Admin access only"
  31. | otherwise = return Authorized
  32. instance RenderMessage App FormMessage where
  33. renderMessage _ _ = defaultFormMessage
  34. -- Hacky YesodAuth instance for just the dummy auth plugin
  35. instance YesodAuth App where
  36. type AuthId App = Text
  37. loginDest _ = HomeR
  38. logoutDest _ = HomeR
  39. getAuthId = return . Just . credsIdent
  40. authPlugins _ = [authDummy]
  41. maybeAuthId = lookupSession credsKey
  42. authHttpManager = error "no http manager provided"
  43. getHomeR :: Handler Html
  44. getHomeR = defaultLayout $ do
  45. setTitle "Route attr homepage"
  46. [whamlet|
  47. <p>
  48. <a href=@{UnprotectedR}>Unprotected
  49. <p>
  50. <a href=@{Admin1R}>Admin 1
  51. <p>
  52. <a href=@{Admin2R}>Admin 2
  53. <p>
  54. <a href=@{Admin3R}>Admin 3
  55. |]
  56. getUnprotectedR, getAdmin1R, getAdmin2R, getAdmin3R :: Handler Html
  57. getUnprotectedR = defaultLayout [whamlet|Unprotected|]
  58. getAdmin1R = defaultLayout [whamlet|Admin1|]
  59. getAdmin2R = defaultLayout [whamlet|Admin2|]
  60. getAdmin3R = defaultLayout [whamlet|Admin3|]
  61. main :: IO ()
  62. main = warp 3000 App

And it was so glaring, I bet you even caught the security hole about Admin3R.