Panel
The panel is the admin area of the app: its address, its configuration, its sign-in, and the messages it shows after a change.
Address
The panel mounts under /admin. The mount method changes the address, with or without a trailing slash:
use ignite_panel::Ignite;
/// The panel at its own address, instead of the default `/admin`.
pub fn panel() -> Ignite {
Ignite::new().mount("/backoffice")
}
The panel's routes are:
GET /admin, the dashboard.GET /admin/login,POST /admin/login,POST /admin/logoutandPOST /admin/logout-everywhere, for sign-in, sign-out and ending every session of the user.GET /admin/<slug>, the list of a resource, withq,sort,dirandpagein the query string.GET /admin/<slug>/newandPOST /admin/<slug>, to create a row.GET /admin/<slug>/<id>/editandPOST /admin/<slug>/<id>, to update a row.POST /admin/<slug>/<id>/delete, to delete a row.
Configuration
These environment variables matter. They can come from the environment or from a .env file in development:
DATABASE_URL, amysql://URL. The panel panics at startup if it is missing or the database cannot be reached.IGNITE_SECRET_KEY, a base64 string of 32 bytes. It signs the sign-in and flash cookies. Create one withopenssl rand -base64 32. A release build stops at startup without a valid key. A debug build uses a random key for that run, with a warning, so sign-ins end when the app restarts.IGNITE_VIEWS_DIR, a folder of the panel's views that replaces the embedded ones, for editing views without a rebuild. It works in debug builds only; a release build ignores it. See Views.
Sign-in
The panel's users live in the users table, which the panel owns: an id, a name, an email address that must be unique, a password hash and a creation time. Passwords are hashed with argon2.
Sign in at /admin/login. A wrong email or password shows the same message for both, so the page does not reveal which emails are registered.
A sign-in is a row in the sessions table, and it lasts eight hours. The browser holds only a signed cookie with the row's id, and the panel checks the row on each request. Sign-out deletes the row. The header's Sign out everywhere button, which posts to /admin/logout-everywhere, deletes every session of the signed-in user.
Every page under the panel needs a session, except sign-in and sign-out. Without one, the request redirects to /admin/login.
The panel has no sign-up page, no password reset, no remember-me and no roles. To add a user, use the command-line tool in Installation. It checks the configuration and the email before it asks for the password:
cargo run --bin ignite -- user create --email you@example.com --name "You"
Forgery protection
Each browser has a token, held in the signed ignite_csrf cookie and created on the first panel page it renders. Every panel form carries it as a hidden _token field. Sign-in, sign-out, log out everywhere, create, update and delete check the token before they do anything. A missing or wrong token gets status 419 and a Page Expired page. Your app's own routes are checked the same way, as the next section shows.
Login throttle
Each failed sign-in counts against its email, five per 60 seconds, and against its IP address, twenty per 60 seconds. The counts are kept in the login_throttles table, so they hold across restarts and processes. While either count is at its limit, sign-in is refused with status 429, a Retry-After header with the seconds to wait, and the sign-in page. A successful sign-in clears the email's count. Requests whose address is unknown share one IP count.
Your own pages
A route of the app under the panel's address can require a sign-in. Take ignite_panel::AdminUser as an argument, and it is a request guard: without a session the request redirects to the sign-in page. user_id() returns the signed-in user's id. It renders the view in Views:
use ignite_panel::AdminUser;
use propel_rocket::PropelView;
use serde::Serialize;
#[derive(Serialize)]
pub struct Account {
pub user_id: i64,
}
/// An app page under the panel's mount. The `AdminUser` guard redirects to the
/// sign-in page when nobody is signed in.
#[rocket::get("/admin/account")]
pub fn account(session: AdminUser) -> PropelView {
PropelView::render(
"account",
&Account {
user_id: session.user_id(),
},
)
}
Forms of your own
An app route that takes a form checks the panel's token. Its data guard is ignite_panel::CsrfForm, which reads _token from the form, or the X-CSRF-TOKEN header, and answers 419 when it is missing or wrong. The page gets the token from ignite_panel::CsrfToken, and writes it into a hidden field. The guard covers POST, PUT, PATCH and DELETE routes.
use ignite_panel::{CsrfForm, CsrfToken};
use propel_rocket::PropelView;
use serde::Serialize;
#[derive(Serialize)]
pub struct Checkout {
pub csrf_token: String,
}
/// The form page. The token comes from the request guard, and the view writes
/// it into a hidden `_token` field.
#[rocket::get("/checkout")]
pub fn checkout(token: CsrfToken) -> PropelView {
PropelView::render(
"checkout",
&Checkout {
csrf_token: token.value().to_string(),
},
)
}
/// The form's POST route. `CsrfForm` checks the token before the handler runs,
/// and answers 419 when it is missing or wrong.
#[rocket::post("/checkout", data = "<form>")]
pub fn place_order(form: CsrfForm) -> String {
form.get("item").unwrap_or_default().to_string()
}
/// A webhook from another service. It has no form token, so the app exempts
/// `/hooks/*` in the builder.
#[rocket::post("/hooks/payment")]
pub fn payment_hook() -> &'static str {
"ok"
}
The form writes the token from its context:
@extends('layouts.app')
@section('title')
Checkout
@endsection
@section('content')
<form method="post" action="/checkout">
<input type="hidden" name="_token" value="{{ $csrf_token }}">
<input type="text" name="item">
<button type="submit">Order</button>
</form>
@endsection
A route that receives webhooks from another service has no form token. Name its path in the builder with csrf_exempt, as .csrf_exempt(&["/hooks/*"]) does here. A path ending in /* covers everything under it. The panel's own routes are always checked.
Messages
After a change, the panel shows a message on the next page. See Notifications.
Ignite