Rewriting Routes in Yesod
January 22, 2020 | Kaliningrad, Russia
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
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.