Runique Admin

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 users model with is_staff and is_superuser fields (generated by model!)
  • The runique binary installed (cargo install runique or cargo build from 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 start regenerates 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")
         .auth(RuniqueAdminAuth::new())
         .routes(admins::routes("/admin"))
         .with_state(admins::admin_state())
    })
    .build()
    .await?
    .run()
    .await?;
MethodRole
.prefix("secret")Segment mounted in front of the admin (default: none) β€” see below
.site_title("…")Title displayed in the interface
.auth(RuniqueAdminAuth::new())Admin authentication (default)
.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 protected by the admin middleware

.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-adminsecret/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 to admins::routes("/backoffice") instead.


Automatic robots.txt β€” When the admin panel is active, Runique automatically serves a /robots.txt route (a Sitemap: line when .sitemap(…) is set, plus the Content-Signal directives). Use .no_robots_txt() if you want to manage this file yourself.

The admin prefix is deliberately absent from it. robots.txt is a public file: writing Disallow: /my-prefix/ would publish the very path a custom prefix is often meant to keep discreet. And Disallow asks 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, nofollow header, 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.

url.rs

pub fn admin_extra_routes() -> Vec<(&'static str, runique::axum::routing::MethodRouter)> {
    vec![
        ("/orders/{number}/detail", 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.