Admin setup
Step-by-step guide to enable the admin interface in an existing Runique project.
Prerequisites
- A working Runique project with a configured database
- A
usersmodel withis_staffandis_superuserfields (generated bymodel!) - The
runiquebinary installed (cargo install runique --features <engine> --lockedorcargo buildfrom the workspace)
Step 1 — Create `src/admin.rs`
This file declares administrable resources via the admin! macro:
// src/admin.rs
use crate::entities::{users, articles};
use crate::forms::{RegisterForm, ArticleForm};
admin! {
users: users::Model => RegisterForm {
title: "Users",
},
articles: articles::Model => ArticleForm {
title: "Articles",
}
}
Step 2 — Generate `src/admins/` with the daemon
runique start
The daemon reads src/admin.rs, generates src/admins/ and launches cargo run.
The src/admins/ folder is created automatically — do not edit it manually.
src/admins/
├── README.md
├── mod.rs
└── admin_panel.rs
If
src/admins/already exists from a previous generation,runique startregenerates it.
Step 3 — Declare the module in `src/main.rs`
mod admin;
mod admins; // module generated by runique start
Step 4 — Wire `.with_admin()` in the builder
use runique::app::builder::RuniqueAppBuilder as builder;
RuniqueApp::builder(config)
.routes(url::routes())
.with_database(db)
.with_admin(|a| {
a.site_title("Administration")
.routes(admins::routes("/admin"))
.with_state(admins::admin_state())
})
.build()
.await?
.run()
.await?;
| Method | Role |
|---|---|
.prefix("secret") | Segment mounted in front of the admin (default: none) — see below |
.site_title("…") | Title displayed in the interface |
.routes(admins::routes("/admin")) | Mounts CRUD routes under /admin |
.with_state(…) | Shared state generated by the daemon |
.no_robots_txt() | Disables the automatic /robots.txt |
.extra_routes(vec![…]) | Custom routes, each behind the right it declares on a resource |
.routes() and .prefix() — two distinct settings
.routes(admins::routes("/site-admin")) sets where the admin lives.
.prefix("secret") sets what is mounted in front of it. They compose:
routes(...) | .prefix(...) | Admin URL |
|---|---|---|
/site-admin | — | /site-admin/… |
/site-admin | secret | /secret/site-admin/… |
| (not called) | secret | /secret/admin/… (default path) |
Call order does not matter, and slashes are optional: secret, /secret and
/secret/ are equivalent.
Version note —
.prefix()used to replace the admin path, putting it in competition with.routes(): the last call won, and swapping the two lines silently changed the served URLs. It now prefixes, as its name promises. A project that used.prefix("/backoffice")to rename the admin should pass that path toadmins::routes("/backoffice")instead.
Automatic robots.txt — When the admin panel is active, Runique automatically serves a
/robots.txtroute (aSitemap:line when.sitemap(…)is set, plus theContent-Signaldirectives). Use.no_robots_txt()if you want to manage this file yourself.The admin prefix is deliberately absent from it.
robots.txtis a public file: writingDisallow: /my-prefix/would publish the very path a custom prefix is often meant to keep discreet. AndDisallowasks crawlers not to fetch, not to index — a URL known from elsewhere can still show up in results.Exclusion is handled by the
X-Robots-Tag: noindex, nofollowheader, which Runique sets on every admin response — login page included, as well as redirects, errors and JSON replies. It leaks no path and stays in place even if you override the admin templates.
Custom admin routes (`extra_routes`)
For business views that go beyond the generated CRUD (e.g. order detail with related lines,
custom dashboard), declare extra routes in url.rs and register them via .extra_routes().
These routes automatically inherit the admin middleware: authentication, AdminState,
PrototypeAdminState (sidebar), and CurrentUser.
Each route also names the resource it works on and the operation it performs
(CrudOperation). The signed-in user needs the matching right on that resource, from their
groups — the same check as the generated pages:
CrudOperation | Right checked |
|---|---|
List, View | can_read |
Create | can_create |
Edit | can_update |
Delete | can_delete |
Without it, the user is sent back to the dashboard with an "insufficient rights" message. A resource key that isn't registered is a build error. Which rows and which columns the handler then reads or writes is up to the handler.
url.rs
use runique::admin::resource::CrudOperation;
use runique::axum::routing::MethodRouter;
pub fn admin_extra_routes() -> Vec<(&'static str, &'static str, CrudOperation, MethodRouter)> {
vec![
("/orders/{number}/detail", "orders", CrudOperation::View, view!{ admin_order_detail }),
]
}
main.rs
.with_admin(|a| a
.routes(admins::routes("/admin"))
.extra_routes(url::admin_extra_routes())
)
Paths are relative to the admin prefix — /orders/{number}/detail becomes
/admin/orders/{number}/detail.
Handler — retrieve the extensions injected by the middleware:
use runique::prelude::*;
use runique::admin::{AdminState, PrototypeAdminState, inject_admin_prefix, insert_admin_messages};
pub async fn admin_order_detail(
Extension(admin): Extension<Arc<AdminState>>,
Extension(proto): Extension<Arc<PrototypeAdminState>>,
mut request: Request,
) -> AppResult<Response> {
inject_admin_prefix(&mut request, &admin.config.prefix);
insert_admin_messages(&mut request, proto.registry.all());
// … business logic …
request.render("admin/order_detail.html")
}
The template must extend admin_base to inherit the CSS and sidebar:
{% extends "admin_base" %}
Step 5 — Create a superuser
runique create-superuser
Follows an interactive wizard to create the first admin account (is_superuser = true).
Accessing the interface
Once the server is running, the interface is available at:
http://localhost:{PORT}/admin/
The /admin/login page redirects to the dashboard if credentials are valid.