use std::sync::Arc;
use axum::{
Extension, Router,
extract::Path,
http::StatusCode,
middleware,
response::{IntoResponse, Redirect, Response},
routing::{get, post},
};
use serde::Deserialize;
use crate::app::staging::AdminStaging;
use crate::auth::{
guard::LoginGuard,
session::{is_admin_authenticated, load_user_middleware, login, logout},
};
use crate::context::template::Request;
use crate::middleware::security::rate_limit_middleware;
use crate::urlpatterns;
use crate::utils::{
aliases::AppResult,
trad::{current_lang, t, tf},
};
use crate::{
admin::{
PrototypeAdminState,
config::AdminConfig,
middleware::admin_required,
trad::{inject_admin_prefix, insert_admin_messages},
},
flash_now,
};
#[derive(Clone)]
pub struct AdminState {
pub config: Arc<AdminConfig>,
pub login_guard: Option<Arc<LoginGuard>>,
}
#[derive(Deserialize)]
struct AdminLoginData {
username: String,
password: String,
#[serde(default)]
csrf_token: String,
}
pub fn build_admin_router(admin_staging: AdminStaging, _db: crate::utils::aliases::ADb) -> Router {
let prefix = admin_staging
.config
.prefix
.trim_end_matches('/')
.to_string();
let config = admin_staging.config;
let state = admin_staging.state;
let login_guard = config.login_guard.clone();
let rate_limiter = config.rate_limiter.clone();
let admin_state = Arc::new(AdminState {
config: Arc::new(config.clone()),
login_guard,
});
let login_get_route = urlpatterns! {
&format!("{prefix}/login") => get(admin_login_get), name = "admin_login",
};
let login_post_route = Router::new().route(&format!("{prefix}/login"), post(admin_login_post));
let login_post_route = if let Some(limiter) = rate_limiter {
login_post_route.layer(middleware::from_fn_with_state(
limiter,
rate_limit_middleware,
))
} else {
login_post_route
};
let public_router = login_get_route.merge(login_post_route);
let protected_router = urlpatterns! {
&format!("{prefix}/") => get(admin_dashboard), name = "admin_dashboard",
&prefix => get(admin_dashboard_redirect), name = "admin_dashboard_redirect",
&format!("{prefix}/logout") => post(admin_logout), name = "admin_logout",
&format!("{prefix}/toggle-template") => get(admin_toggle_template), name = "admin_toggle_template",
&format!("{prefix}/history") => get(admin_history), name = "admin_history",
&format!("{prefix}/history/timeline") => get(admin_history_timeline), name = "admin_history_timeline",
&format!("{prefix}/history/batch/{{batch_id}}") => get(admin_history_batch), name = "admin_history_batch",
&format!("{prefix}/history/{{id}}") => get(admin_history_diff), name = "admin_history_diff",
};
let generated_router = if let Some(router) = admin_staging.route_admin {
router
} else {
Router::new()
};
let generated_router = admin_staging
.extra_routes
.into_iter()
.fold(generated_router, |router, (path, method_router)| {
router.route(&format!("{}{}", prefix, path), method_router)
});
let mut router = public_router
.merge(
protected_router
.merge(generated_router)
.layer(middleware::from_fn(admin_required)),
)
.layer(middleware::from_fn_with_state(_db, load_user_middleware))
.layer(Extension(admin_state));
if let Some(state) = state {
let order = config.resource_order.clone();
let config = Arc::new(config);
let registry = match Arc::try_unwrap(state) {
Ok(proto) => match Arc::try_unwrap(proto.registry) {
Ok(mut reg) => {
if !order.is_empty() {
reg.reorder(&order);
}
Arc::new(reg)
}
Err(arc) => arc,
},
Err(arc) => arc.registry.clone(),
};
let merged = Arc::new(PrototypeAdminState { registry, config });
router = router.layer(Extension(merged));
}
router
}
pub const ADMIN_TEMPLATE_SESSION_KEY: &str = "admin_template_override";
async fn admin_dashboard_redirect(Extension(admin): Extension<Arc<AdminState>>) -> Response {
Redirect::permanent(&format!("{}/", admin.config.prefix.trim_end_matches('/'))).into_response()
}
async fn admin_dashboard(
Extension(admin): Extension<Arc<AdminState>>,
Extension(current_user): Extension<crate::auth::session::CurrentUser>,
proto: Option<Extension<Arc<PrototypeAdminState>>>,
mut req: Request,
) -> AppResult<Response> {
let db = req.engine.db.clone();
let mut resource_counts: std::collections::HashMap<String, u64> =
std::collections::HashMap::new();
let resources: Vec<&crate::admin::AdminResource> = if let Some(Extension(ref state)) = proto {
for (key, entry) in &state.registry.resources {
if let Some(count_fn) = &entry.count_fn
&& let Ok(n) = (count_fn)(db.clone(), None).await
{
resource_counts.insert(key.clone(), n);
}
}
state
.registry
.all()
.filter(|e| {
if current_user.is_superuser {
return true;
}
current_user.can_access_resource(e.meta.key)
})
.map(|e| &e.meta)
.collect()
} else {
Vec::new()
};
let resource_groups: std::collections::HashMap<String, Vec<String>> = {
use crate::admin::permissions::{groupe, groupes_droits};
use sea_orm::EntityTrait;
let groupes: std::collections::HashMap<_, String> = groupe::Entity::find()
.all(&*db)
.await
.unwrap_or_default()
.into_iter()
.map(|g| (g.id, g.nom))
.collect();
let rows = groupes_droits::Entity::find()
.all(&*db)
.await
.unwrap_or_default();
let mut map: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for r in rows {
let nom = groupes
.get(&r.groupe_id)
.cloned()
.unwrap_or_else(|| r.groupe_id.to_string());
let entry = map.entry(r.resource_key).or_default();
if !entry.contains(&nom) {
entry.push(nom);
}
}
map
};
let session_override: Option<String> = req
.session
.get(ADMIN_TEMPLATE_SESSION_KEY)
.await
.unwrap_or(None);
insert_admin_messages(&mut req.context, "dashboard");
insert_admin_messages(&mut req.context, "base");
inject_admin_prefix(&mut req.context, &admin.config.prefix);
req = req
.insert("current_user", ¤t_user)
.insert("site_title", &admin.config.site_title)
.insert("site_url", &admin.config.site_url)
.insert("resources", &resources)
.insert("resource_groups", &resource_groups)
.insert("resource_counts", &resource_counts)
.insert("current_page", "dashboard")
.insert("lang", current_lang().code())
.insert("current_resource", &Option::<String>::None)
.insert("admin_has_session_override", session_override.is_some());
let template = session_override
.as_deref()
.unwrap_or_else(|| admin.config.templates.dashboard.resolve());
req.render(template)
}
async fn admin_login_get(
Extension(admin): Extension<Arc<AdminState>>,
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
mut req: Request,
) -> AppResult<Response> {
let from_logout = params.get("from").is_some_and(|v| v == "logout");
if !from_logout && is_admin_authenticated(&req.session).await {
return Ok(Redirect::to(&format!("{}/", admin.config.prefix)).into_response());
}
insert_admin_messages(&mut req.context, "login");
inject_admin_prefix(&mut req.context, &admin.config.prefix);
req = req
.insert("site_title", &admin.config.site_title)
.insert("site_url", &admin.config.site_url)
.insert("lang", current_lang().code());
req.render(admin.config.templates.login.resolve())
}
async fn admin_login_post(
Extension(admin): Extension<Arc<AdminState>>,
mut req: Request,
) -> Response {
use crate::utils::middleware::csrf::unmask_csrf_token;
use subtle::ConstantTimeEq;
if is_admin_authenticated(&req.session).await {
return Redirect::to(&format!("{}/", admin.config.prefix)).into_response();
}
let data = AdminLoginData {
username: req.prisme.data.get("username").cloned().unwrap_or_default(),
password: req.prisme.data.get("password").cloned().unwrap_or_default(),
csrf_token: req
.prisme
.data
.get(crate::utils::constante::session_key::session::CSRF_TOKEN_KEY)
.cloned()
.unwrap_or_default(),
};
let csrf_valid = unmask_csrf_token(&data.csrf_token)
.map(|unmasked| {
bool::from(
unmasked
.as_bytes()
.ct_eq(req.csrf_token.as_str().as_bytes()),
)
})
.unwrap_or(false);
if !csrf_valid {
insert_admin_messages(&mut req.context, "login");
inject_admin_prefix(&mut req.context, &admin.config.prefix);
req = req
.insert("lang", current_lang().code())
.insert("site_title", &admin.config.site_title)
.insert("site_url", &admin.config.site_url)
.insert("error", t("csrf.invalid_or_missing").to_string());
return req
.render(admin.config.templates.login.resolve())
.unwrap_or_else(axum::response::IntoResponse::into_response);
}
if let Some(guard) = &admin.login_guard {
let key = LoginGuard::effective_key(&data.username, "unknown");
if guard.is_locked(&key) {
let secs = guard.remaining_lockout_secs(&key).unwrap_or(0);
insert_admin_messages(&mut req.context, "login");
inject_admin_prefix(&mut req.context, &admin.config.prefix);
req = req
.insert("lang", current_lang().code())
.insert("site_title", &admin.config.site_title)
.insert("site_url", &admin.config.site_url)
.insert("error", tf("admin.login.error_locked", &[secs]));
return req
.render(admin.config.templates.login.resolve())
.unwrap_or_else(axum::response::IntoResponse::into_response);
}
}
let Some(auth) = &admin.config.auth else {
return (
StatusCode::NOT_IMPLEMENTED,
t("admin.access.no_auth_handler").to_string(),
)
.into_response();
};
let result = auth
.authenticate(&data.username, &data.password, &req.engine.db)
.await;
if let Some(user) = result {
if let Some(guard) = &admin.login_guard {
let key = LoginGuard::effective_key(&data.username, "unknown");
guard.record_success(&key);
}
let db_store = req
.engine
.session_db_store
.read()
.ok()
.and_then(|g| g.as_ref().cloned());
let exclusive = req.engine.features.exclusive_login;
if login(
&req.session,
&req.engine.db,
user.user_id,
&user.username,
user.is_staff,
user.is_superuser,
db_store.as_deref(),
exclusive,
)
.await
.is_err()
{
insert_admin_messages(&mut req.context, "login");
insert_admin_messages(&mut req.context, "base");
inject_admin_prefix(&mut req.context, &admin.config.prefix);
req = req
.insert("lang", current_lang().code())
.insert("site_title", &admin.config.site_title)
.insert("site_url", &admin.config.site_url)
.insert("error", t("admin.login.error_session").to_string());
return req
.render(admin.config.templates.login.resolve())
.unwrap_or_else(axum::response::IntoResponse::into_response);
}
Redirect::to(&format!("{}/", admin.config.prefix)).into_response()
} else {
if let Some(guard) = &admin.login_guard {
let key = LoginGuard::effective_key(&data.username, "unknown");
guard.record_failure(&key);
}
insert_admin_messages(&mut req.context, "login");
insert_admin_messages(&mut req.context, "base");
inject_admin_prefix(&mut req.context, &admin.config.prefix);
req = req
.insert("lang", current_lang().code())
.insert("site_title", &admin.config.site_title)
.insert("site_url", &admin.config.site_url)
.insert("error", t("admin.login.error_credentials").to_string());
req.render(admin.config.templates.login.resolve())
.unwrap_or_else(axum::response::IntoResponse::into_response)
}
}
async fn admin_history(
Extension(admin): Extension<Arc<AdminState>>,
Extension(current_user): Extension<crate::auth::session::CurrentUser>,
proto: Option<Extension<Arc<PrototypeAdminState>>>,
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
mut req: Request,
) -> AppResult<Response> {
use crate::admin::history;
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder};
if !current_user.is_staff && !current_user.is_superuser {
req.notices
.error(t("admin.access.insufficient_rights").to_string())
.await;
return Ok(
axum::response::Redirect::to(&format!("{}/", admin.config.prefix)).into_response(),
);
}
let page = params
.get("page")
.and_then(|p| p.parse::<u64>().ok())
.unwrap_or(1)
.max(1);
let per_page = admin.config.page_size;
let filter_resource: Option<String> = params.get("resource").filter(|v| !v.is_empty()).cloned();
let filter_action: Option<String> = params.get("action").filter(|v| !v.is_empty()).cloned();
let filter_user: Option<String> = params.get("user").filter(|v| !v.is_empty()).cloned();
let mut query = history::Entity::find().order_by_desc(history::Column::CreatedAt);
if let Some(ref res) = filter_resource {
query = query.filter(history::Column::ResourceKey.eq(res.clone()));
}
if let Some(ref action) = filter_action {
query = query.filter(history::Column::Action.eq(action.clone()));
}
if let Some(ref user) = filter_user {
query = query.filter(history::Column::Username.eq(user.clone()));
}
let paginator = query.paginate(req.engine.db.as_ref(), per_page);
let total_pages = paginator.num_pages().await.unwrap_or(1);
let raw_entries: Vec<history::Model> = paginator.fetch_page(page - 1).await.unwrap_or_default();
#[derive(serde::Serialize)]
struct HistoryRow {
id: i64,
resource_key: String,
object_pk: String,
action: String,
username: String,
created_at: chrono::NaiveDateTime,
summary: Option<String>,
batch_id: Option<String>,
batch_count: usize,
}
let mut entries: Vec<HistoryRow> = Vec::new();
let mut seen_batches: std::collections::HashSet<String> = std::collections::HashSet::new();
for e in raw_entries {
if let Some(ref bid) = e.batch_id {
if seen_batches.contains(bid) {
if let Some(row) = entries
.iter_mut()
.find(|r| r.batch_id.as_deref() == Some(bid))
{
row.batch_count += 1;
}
continue;
}
seen_batches.insert(bid.clone());
}
entries.push(HistoryRow {
id: e.id,
resource_key: e.resource_key,
object_pk: e.object_pk,
action: e.action,
username: e.username,
created_at: e.created_at,
summary: e.summary,
batch_id: e.batch_id,
batch_count: 1,
});
}
let resources: Vec<&crate::admin::AdminResource> = if let Some(Extension(ref state)) = proto {
state
.registry
.all()
.filter(|e| current_user.is_superuser || current_user.can_access_resource(e.meta.key))
.map(|e| &e.meta)
.collect()
} else {
Vec::new()
};
insert_admin_messages(&mut req.context, "base");
inject_admin_prefix(&mut req.context, &admin.config.prefix);
req = req
.insert("entries", &entries)
.insert("page", page)
.insert("total_pages", total_pages)
.insert("filter_resource", &filter_resource)
.insert("filter_action", &filter_action)
.insert("filter_user", &filter_user)
.insert("current_page", "history")
.insert("current_resource", &Option::<String>::None)
.insert("resources", &resources)
.insert("current_user", ¤t_user)
.insert("site_title", &admin.config.site_title)
.insert("site_url", &admin.config.site_url)
.insert("lang", current_lang().code());
req.render("admin/history")
}
async fn admin_history_diff(
Path(entry_id): Path<i64>,
Extension(admin): Extension<Arc<AdminState>>,
Extension(current_user): Extension<crate::auth::session::CurrentUser>,
proto: Option<Extension<Arc<PrototypeAdminState>>>,
mut req: Request,
) -> AppResult<Response> {
use crate::admin::history;
use crate::context::template::AppError;
use crate::errors::error::ErrorContext;
use sea_orm::EntityTrait;
if !current_user.is_staff && !current_user.is_superuser {
req.notices
.error(t("admin.access.insufficient_rights").to_string())
.await;
return Ok(
axum::response::Redirect::to(&format!("{}/", admin.config.prefix)).into_response(),
);
}
let entry = history::Entity::find_by_id(entry_id)
.one(req.engine.db.as_ref())
.await
.map_err(|e| Box::new(AppError::new(ErrorContext::database(e))))?
.ok_or_else(|| Box::new(AppError::new(ErrorContext::not_found("Entry not found"))))?;
#[derive(serde::Serialize)]
struct DiffField {
key: String,
old_val: String,
new_val: String,
}
let diff_fields: Vec<DiffField> = entry
.summary
.as_deref()
.and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
.and_then(|v| v.as_object().cloned())
.map(|map| {
let mut fields: Vec<DiffField> = map
.into_iter()
.filter_map(|(k, v)| {
let obj = v.as_object()?;
let str_val = |v: &serde_json::Value| match v {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
Some(DiffField {
key: k,
old_val: obj.get("old").map(str_val).unwrap_or_default(),
new_val: obj.get("new").map(str_val).unwrap_or_default(),
})
})
.collect();
fields.sort_by(|a, b| a.key.cmp(&b.key));
fields
})
.unwrap_or_default();
let resources: Vec<&crate::admin::AdminResource> = if let Some(Extension(ref state)) = proto {
state
.registry
.all()
.filter(|e| current_user.is_superuser || current_user.can_access_resource(e.meta.key))
.map(|e| &e.meta)
.collect()
} else {
Vec::new()
};
insert_admin_messages(&mut req.context, "base");
inject_admin_prefix(&mut req.context, &admin.config.prefix);
req = req
.insert("entry", &entry)
.insert("diff_fields", &diff_fields)
.insert("current_page", "history")
.insert("current_resource", &Option::<String>::None)
.insert("resources", &resources)
.insert("current_user", ¤t_user)
.insert("site_title", &admin.config.site_title)
.insert("site_url", &admin.config.site_url)
.insert("lang", current_lang().code());
req.render("admin/history_diff")
}
async fn admin_history_timeline(
Extension(admin): Extension<Arc<AdminState>>,
Extension(current_user): Extension<crate::auth::session::CurrentUser>,
proto: Option<Extension<Arc<PrototypeAdminState>>>,
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
mut req: Request,
) -> AppResult<Response> {
use crate::admin::history;
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder};
if !current_user.is_staff && !current_user.is_superuser {
req.notices
.error(t("admin.access.insufficient_rights").to_string())
.await;
return Ok(
axum::response::Redirect::to(&format!("{}/", admin.config.prefix)).into_response(),
);
}
let page = params
.get("page")
.and_then(|p| p.parse::<u64>().ok())
.unwrap_or(1)
.max(1);
let per_page = admin.config.page_size;
let filter_user_id: Option<crate::utils::Pk> =
params.get("user_id").and_then(|v| v.parse().ok());
let filter_resource: Option<String> = params.get("resource").filter(|v| !v.is_empty()).cloned();
let filter_object_id: Option<String> =
params.get("object_id").filter(|v| !v.is_empty()).cloned();
let mut query = history::Entity::find().order_by_desc(history::Column::CreatedAt);
if let Some(uid) = filter_user_id {
query = query.filter(history::Column::UserId.eq(uid));
}
if let Some(ref res) = filter_resource {
query = query.filter(history::Column::ResourceKey.eq(res.clone()));
}
if let Some(ref oid) = filter_object_id {
query = query.filter(history::Column::ObjectPk.eq(oid.clone()));
}
let paginator = query.paginate(req.engine.db.as_ref(), per_page);
let total_pages = paginator.num_pages().await.unwrap_or(1);
let total = paginator.num_items().await.unwrap_or(0);
let entries: Vec<history::Model> = paginator.fetch_page(page - 1).await.unwrap_or_default();
let filter_type = if filter_object_id.is_some() && filter_resource.is_some() {
"object"
} else if filter_user_id.is_some() {
"user"
} else if filter_resource.is_some() {
"resource"
} else {
"all"
};
let resources: Vec<&crate::admin::AdminResource> = if let Some(Extension(ref state)) = proto {
state
.registry
.all()
.filter(|e| current_user.is_superuser || current_user.can_access_resource(e.meta.key))
.map(|e| &e.meta)
.collect()
} else {
Vec::new()
};
insert_admin_messages(&mut req.context, "base");
inject_admin_prefix(&mut req.context, &admin.config.prefix);
req = req
.insert("entries", &entries)
.insert("page", page)
.insert("total_pages", total_pages)
.insert("total", total)
.insert("filter_type", filter_type)
.insert("filter_user_id", filter_user_id)
.insert("filter_resource", &filter_resource)
.insert("filter_object_id", &filter_object_id)
.insert("current_page", "history")
.insert("current_resource", &Option::<String>::None)
.insert("resources", &resources)
.insert("current_user", ¤t_user)
.insert("site_title", &admin.config.site_title)
.insert("site_url", &admin.config.site_url)
.insert("lang", current_lang().code());
req.render("admin/history_timeline")
}
async fn admin_history_batch(
Path(batch_id): Path<String>,
Extension(admin): Extension<Arc<AdminState>>,
Extension(current_user): Extension<crate::auth::session::CurrentUser>,
proto: Option<Extension<Arc<PrototypeAdminState>>>,
mut req: Request,
) -> AppResult<Response> {
use crate::admin::history;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
if !current_user.is_staff && !current_user.is_superuser {
req.notices
.error(t("admin.access.insufficient_rights").to_string())
.await;
return Ok(
axum::response::Redirect::to(&format!("{}/", admin.config.prefix)).into_response(),
);
}
let entries: Vec<history::Model> = history::Entity::find()
.filter(history::Column::BatchId.eq(batch_id.clone()))
.order_by_asc(history::Column::Id)
.all(req.engine.db.as_ref())
.await
.unwrap_or_default();
#[derive(serde::Serialize)]
struct DiffField {
key: String,
old_val: String,
new_val: String,
}
#[derive(serde::Serialize)]
struct BatchEntry {
id: i64,
object_pk: String,
display_name: Option<String>,
action: String,
summary: Option<String>,
diff_fields: Vec<DiffField>,
}
fn parse_diff(summary: Option<&str>) -> Vec<DiffField> {
summary
.and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
.and_then(|v| v.as_object().cloned())
.map(|map| {
let mut fields: Vec<DiffField> = map
.into_iter()
.filter_map(|(k, v)| {
let obj = v.as_object()?;
let str_val = |v: &serde_json::Value| match v {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
Some(DiffField {
key: k,
old_val: obj.get("old").map(str_val).unwrap_or_default(),
new_val: obj.get("new").map(str_val).unwrap_or_default(),
})
})
.collect();
fields.sort_by(|a, b| a.key.cmp(&b.key));
fields
})
.unwrap_or_default()
}
fn extract_display_name(val: &serde_json::Value) -> Option<String> {
let obj = val.as_object()?;
for key in &["username", "name", "title", "email", "label", "slug"] {
if let Some(serde_json::Value::String(s)) = obj.get(*key)
&& !s.is_empty()
{
return Some(s.clone());
}
}
None
}
let resource_key = entries
.first()
.map(|e| e.resource_key.clone())
.unwrap_or_default();
let username = entries
.first()
.map(|e| e.username.clone())
.unwrap_or_default();
let created_at = entries.first().map(|e| e.created_at);
let action = entries
.first()
.map(|e| e.action.clone())
.unwrap_or_default();
let get_fn = proto
.as_ref()
.and_then(|Extension(state)| state.registry.get(&resource_key))
.and_then(|entry| entry.get_fn.clone());
let mut batch_entries: Vec<BatchEntry> = Vec::new();
for e in entries {
let display_name = if let Some(ref f) = get_fn {
f(req.engine.db.clone(), e.object_pk.clone())
.await
.ok()
.flatten()
.as_ref()
.and_then(extract_display_name)
} else {
None
};
let diff = parse_diff(e.summary.as_deref());
batch_entries.push(BatchEntry {
id: e.id,
object_pk: e.object_pk,
display_name,
action: e.action,
summary: e.summary,
diff_fields: diff,
});
}
let resources: Vec<&crate::admin::AdminResource> = if let Some(Extension(ref state)) = proto {
state
.registry
.all()
.filter(|e| current_user.is_superuser || current_user.can_access_resource(e.meta.key))
.map(|e| &e.meta)
.collect()
} else {
Vec::new()
};
insert_admin_messages(&mut req.context, "base");
inject_admin_prefix(&mut req.context, &admin.config.prefix);
req = req
.insert("batch_id", &batch_id)
.insert("batch_entries", &batch_entries)
.insert("resource_key", &resource_key)
.insert("username", &username)
.insert("created_at", created_at)
.insert("action", &action)
.insert("current_page", "history")
.insert("current_resource", &Option::<String>::None)
.insert("resources", &resources)
.insert("current_user", ¤t_user)
.insert("site_title", &admin.config.site_title)
.insert("site_url", &admin.config.site_url)
.insert("lang", current_lang().code());
req.render("admin/history_batch")
}
async fn admin_toggle_template(
Extension(admin): Extension<Arc<AdminState>>,
req: Request,
) -> Response {
let current: Option<String> = req
.session
.get::<String>(ADMIN_TEMPLATE_SESSION_KEY)
.await
.unwrap_or(None);
if current.is_some() {
let _ = req
.session
.remove::<String>(ADMIN_TEMPLATE_SESSION_KEY)
.await;
} else {
let _ = req
.session
.insert(ADMIN_TEMPLATE_SESSION_KEY, "admin/dashboard")
.await;
}
Redirect::to(&format!("{}/", admin.config.prefix)).into_response()
}
async fn admin_logout(Extension(admin): Extension<Arc<AdminState>>, req: Request) -> Response {
let session = &req.session;
let db_store = req
.engine
.session_db_store
.read()
.ok()
.and_then(|g| g.as_ref().cloned());
let _ = logout(session, db_store.as_deref()).await;
let login_url = format!("{}/login?from=logout", admin.config.prefix);
flash_now!(info => t("admin.logout_success").as_ref());
Redirect::to(&login_url).into_response()
}