Add messages so users can copy out the API key after creating it

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2026-01-25 18:47:34 +01:00
parent 7a5233e385
commit 6a5acaab32
5 changed files with 73 additions and 7 deletions

18
Cargo.lock generated
View file

@ -209,6 +209,22 @@ dependencies = [
"syn",
]
[[package]]
name = "axum-messages"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d67ce6e7bc1e1e71f2a4e86d418045a29c63c4ebb631f3d9bb2f81c4958ea391"
dependencies = [
"axum-core",
"http",
"parking_lot",
"serde",
"serde_json",
"tower",
"tower-sessions-core",
"tracing",
]
[[package]]
name = "base64"
version = "0.22.1"
@ -1410,12 +1426,14 @@ dependencies = [
"anyhow",
"axum",
"axum-login",
"axum-messages",
"displaydoc",
"notify-debouncer-full",
"password-auth",
"rand 0.9.2",
"serde",
"serde_json",
"sha2",
"sqlx",
"tera",
"thiserror 2.0.17",

View file

@ -27,3 +27,5 @@ time = "0.3.45"
rand = "0.9.2"
serde_json.workspace = true
tower-http = { version = "0.6.8", features = ["normalize-path", "fs"] }
sha2 = "0.10.9"
axum-messages = "0.8.0"

View file

@ -9,6 +9,8 @@ use axum::response::IntoResponse;
use axum::routing::get;
use axum_login::AuthManagerLayerBuilder;
use axum_login::AuthnBackend;
use axum_messages::Messages;
use axum_messages::MessagesManagerLayer;
use displaydoc::Display;
use notify_debouncer_full::DebouncedEvent;
use notify_debouncer_full::notify::EventKind;
@ -169,6 +171,7 @@ async fn run() -> anyhow::Result<()> {
ServeDir::new("public").append_index_html_on_directories(false),
)
.route("/", get(show_index))
.layer(MessagesManagerLayer)
.layer(auth_layer)
.layer(NormalizePathLayer::trim_trailing_slash())
.layer(livereload)
@ -245,6 +248,7 @@ async fn shutdown_signal(handle: AbortHandle) {
#[derive(Debug, FromRequestParts)]
pub struct Renderer {
auth: AuthSession,
messages: Messages,
}
impl Renderer {
@ -259,6 +263,16 @@ impl Renderer {
main_context.insert("current_user", user);
}
let messages = self
.messages
.clone()
.map(|message| serde_json::json!({ "level": message.level.to_string().to_ascii_lowercase(), "text": message.message}))
.collect::<Vec<_>>();
if !messages.is_empty() {
main_context.insert("messages", &messages);
}
main_context.extend(context.into().unwrap_or_default());
Ok(Html(TERA.read().unwrap().render(name, &main_context)?))

View file

@ -6,6 +6,7 @@ use axum::response::Redirect;
use axum::routing::get;
use axum::routing::post;
use axum_login::login_required;
use axum_messages::Messages;
use password_auth::generate_hash;
use password_auth::verify_password;
use rand::distr::Alphanumeric;
@ -13,11 +14,11 @@ use rand::distr::SampleString;
use rand::rng;
use serde::Deserialize;
use serde::Serialize;
use sha2::Digest;
use sqlx::prelude::FromRow;
use tera::Context;
use time::Date;
use time::OffsetDateTime;
use time::Time;
use crate::AppState;
use crate::AuthSession;
@ -141,12 +142,20 @@ struct NewApiKeyForm {
async fn create_new_api_key(
app_state: State<AppState>,
auth: AuthSession,
messages: Messages,
new_api_key: Form<NewApiKeyForm>,
) -> WebResult<impl IntoResponse> {
let values = Alphanumeric.sample_string(&mut rng(), 32);
let token = tokio::task::spawn_blocking(|| generate_hash(values))
let api_key = Alphanumeric
.sample_string(&mut rng(), 32)
.to_ascii_lowercase();
let token = tokio::task::spawn_blocking({
let api_key = api_key.clone();
move || sha2::Sha256::digest(&api_key)
})
.await
.unwrap();
.unwrap()
.to_vec();
let expiration_date = Date::parse(
&new_api_key.expiration_date,
@ -154,8 +163,9 @@ async fn create_new_api_key(
)
.unwrap();
let expiration_date =
OffsetDateTime::new_utc(expiration_date, Time::from_hms(0, 0, 0).unwrap());
let expiration_date = expiration_date
.format(&time::format_description::well_known::iso8601::Iso8601::DATE)
.unwrap();
sqlx::query("INSERT INTO api_keys (user_id, token, name, expiration_date, permissions, revoked) VALUES (?, ?, ?, ?, ?, false)")
.bind(auth.user.unwrap().id())
@ -166,6 +176,8 @@ async fn create_new_api_key(
.execute(&app_state.db)
.await?;
messages.success(format!("Your API Token is created: {api_key}. It will only be displayed once. Be sure to copy it now."));
Ok(Redirect::to("/settings/api_keys"))
}

View file

@ -42,6 +42,26 @@
</div>
</nav>
<main class="grow px-2 flex flex-col inset-shadow-sm">
<div class="m-4 flex flex-col">
{% for message in messages | default(value=[])%}
{% if message.level == "info" %}
{% set color = "bg-cyan-300" %}
{% set border_color = "border-cyan-900" %}
{% elif message.level == "success" %}
{% set color = "bg-blue-300" %}
{% set border_color = "border-blue-900" %}
{% elif message.level == "error" %}
{% set color = "bg-blue-300" %}
{% set border_color = "border-blue-900" %}
{% else %}
{% set color = "bg-zinc-300" %}
{% set border_color = "border-zinc-900" %}
{% endif %}
<div class="p-4 {{color}} rounded shadow-md border {{border_color}}">
{{ message.text }}
</div>
{% endfor %}
</div>
<div class="grow flex mx-auto container">
{% block content %}{% endblock content %}
</div>