Rewriting Routes in Yesod
User authentication in Yesod typically works through a plugin system, also known as a subsite. This is handy because it means your web application can support multiple methods of authentication simultaneously and you can also swap out one system for another relatively painlessly.
One drawback of this system however is the routes end up looking quite verbose.
For example, a user of your website might expect the login route to be located
at /login
. With the plugin system however, that page will be located at
/auth/page/foo/login
, where foo
is the name of the plugin the user intends
to use.
Since Yesod runs on WAI, we can solve the problem of verbose routes by rewriting them with a WAI middleware. This is pretty straightforward and of course works for more than just authentication routes, though they make for a good example.
The first step is pulling in the right library. The functions we want to use are in wai-extra, so add that to your cabal file if it isn’t already there. If you used a template like yesod-postgres to start your project, you likely already have this package available.
Next we need to override the way Yesod renders routes for us. The Yesod
typeclass exposes a method called urlParamRenderOverride
. We can
use this to translate a typesafe route into whatever other string
representation we need, while also retaining any query string parameters passed
along.
The example below changes the way auth routes from the yesod-auth-simple auth plugin are rendered. Your own implementation may slightly differ.
instance Yesod App where
-- other typeclass method overrides
urlParamRenderOverride y r _ =
let root = fromMaybe "" (appRoot (appSettings y))
toRoute p = Just $ uncurry (joinPath y root) (p, [])
in case r of
(AuthR LoginR) -> toRoute ["login"]
(AuthR LogoutR) -> toRoute ["logout"]
(AuthR (PluginR "simple" p)) -> toRoute p
_ -> Nothing
-- yet more custom stuff
Finally, we want to intercept user requests to the shortened paths and expand
them to their full form under the hood. This is where our WAI middleware comes
in. Pay special attention to the final pattern match of the rw
function — you
want all other routes in your application to pass through unaltered.
import Network.Wai.Middleware.Rewrite (rewritePureWithQueries)
rewriteAuthRoutes :: Middleware
rewriteAuthRoutes = rewritePureWithQueries rw
where
plugin :: [Text]
plugin = ["auth", "page", "simple"]
rw :: PathsAndQueries -> RequestHeaders -> PathsAndQueries
rw (["register"], _) _ = (plugin <> ["register"], [])
rw (["confirm", token], _) _ = (plugin <> ["confirm", token], [])
rw (["confirmation-email-sent"], _) _ = (plugin <> ["confirmation-email-sent"], [])
rw (["login"], _) _ = (plugin <> ["login"], [])
rw (["reset-password"], _) _ = (plugin <> ["reset-password"], [])
rw (["reset-password-email-sent"], _) _ = (plugin <> ["reset-password-email-sent"], [])
rw (["logout"], _) _ = (["auth", "logout"], [])
rw (path, qs) _ = (path, qs)
makeApplication :: App -> IO Application
makeApplication foundation = do
logWare <- makeLogWare foundation
appPlain <- toWaiAppPlain foundation
initState foundation
let middlewares = defaultMiddlewaresNoLogging
>>> logWare
>>> rewriteAuthRoutes
return $ middlewares appPlain
Connect your custom WAI middleware to our middleware chain when building the application, and everything should work as you expect.