Howdy Admin
howdy_admin is an admin area for your app while you develop it. It is
mounted from your dev/ entry point, beside howdy_dev, so it never
reaches a deployment. Depending on what you give it, it lets you:
- browse and edit the database, in a grid that follows the data as it changes
- manage users, groups and roles, and sign in to the app as any user
- read the mail your app sends, and preview your email templates
- follow each request as a timeline of its queries, calls, emails and logs
Install
Add the package to the dev dependencies in gleam.toml, next to
howdy_dev:
[dev_dependencies]
howdy_dev = { git = "https://github.com/mikeyjones/howdy.git", ref = "v2.0.0", path = "howdy_dev" }
howdy_admin = { git = "https://github.com/mikeyjones/howdy.git", ref = "v2.0.0", path = "admin" }
Mounting it
admin.mount adds the admin's routes to the app your dev/ entry point
serves:
// dev/my_app_dev.gleam
import gleam/erlang/process
import howdy/admin
import howdy/dev
import my_app
pub fn main() {
let db = my_app.open_database()
let identity = my_app.identity(db)
let permissions = my_app.permissions(db)
let dashboard =
admin.new()
|> admin.auth(identity)
|> admin.authorization(permissions)
let assert Ok(_) =
dev.start(fn() {
my_app.app(db, identity, permissions) |> admin.mount(dashboard)
})
process.sleep_forever()
}
Run howdy dev and open
http://localhost:8787/_howdy, on whichever port your app listens on.
Build the database, auth and anything else the admin is given outside the
function dev.start rebuilds the app from, as here, so they are made once
and not on every reload.
What you register is what it shows
Gleam cannot find out at runtime which packages an app uses, and there is no registry of the values it built, so the app hands the admin what it has:
-
admin.database(repo)shows the tables of a Gloo Repo, on PostgreSQL or SQLite. See Database browser. -
admin.auth(identity)shows users and groups from Howdy Auth. It registers auth's Repo as the database too, unlessadmin.databasewas given another. See Users and roles. -
admin.authorization(permissions)shows roles and permissions. It needsadmin.authtoo, since roles are held by users. -
admin.mail(box)shows what ahowdy/mail/outboxkeeps, andadmin.mail_previews(previews, send_with: mailer)shows your email templates. See Outbox and previews.
The first page lists what was registered, and the sidebar links to each part. A part that was not registered is not mounted at all.
Some things need no registering, because admin.mount can see them in the
app itself. If the app serves an OpenAPI document with howdy/openapi, the
admin lists its endpoints and lets you call them, as any user. See
Calling your API. Mount the admin last, so it sees the
whole app.
Options
admin.new()
|> admin.at("/admin")
|> admin.named("Acme admin")
|> admin.database(db)
-
admin.at(prefix)moves the pages from/_howdy. The prefix cannot be/. -
admin.named(name)sets the title in the sidebar, "Howdy admin" by default. -
admin.allow_hosts(hosts)changes which host names the pages answer. See below. -
admin.prefix(admin)is where the pages are, for linking to them from your own dev pages.
admin.mount(app, admin) adds every controller to the app.
admin.controllers(admin) gives you the controllers instead, to mount
yourself.
Development only
There is no login. Anyone who can reach the pages can read and change every row, and sign in to your app as anyone. These are the defences:
-
It is a dev dependency, and it is mounted from
dev/, whichgleam export erlang-shipmentleaves out. -
howdy_devlistens on loopback only. -
The pages answer
403to any request whoseHostis notlocalhost,127.0.0.1or[::1], so a page on another site cannot reach them through DNS rebinding. -
Writes must come from the admin's own origin. A
POSTwhoseSec-Fetch-SiteorOriginheader says another site sent it gets403, so a page you visit while the admin runs cannot post to it. Forms carry no CSRF token; the browser's headers do that job.
To open the admin from another machine on a network you trust, list the exact host names it will be reached by:
admin.new()
|> admin.allow_hosts(["localhost", "devbox.local"])
allow_hosts replaces the loopback names, and takes host names without a
port, a scheme or wildcards. howdy_dev has its own dev.bind and
dev.allow_hosts for the server itself. Anyone who can reach an allowed
host owns your data.
