rok-core 0.6.1

Core primitives for the rok ecosystem — errors, crypto, i18n, config, DI, and more
Documentation
//! URL generation utilities including named routes and signed URLs.
//!
//! Requires `features = ["named-routes"]`.  Signed URLs additionally require
//! `features = ["crypto"]`.

use crate::named_routes;

/// Generate a URL for a named route, substituting path parameters.
///
/// # Examples
///
/// ```rust,ignore
/// // Register routes at startup
/// rok_core::named_routes::register("posts.show", "/posts/:id");
///
/// // Later, in a handler:
/// let url = rok_core::url::route_url("posts.show", &[("id", "42")]);
/// assert_eq!(url, "/posts/42");
/// ```
pub fn route_url(name: &str, params: &[(&str, &str)]) -> String {
    named_routes::resolve(name, params)
}

/// Generate a signed URL for a named route with an expiration timestamp.
///
/// The URL includes `expires` and `signature` query parameters. The signature
/// is an HMAC-SHA256 of the URL path plus the expiration time, signed with
/// the application's secret key.
///
/// # Examples
///
/// ```rust,ignore
/// let url = rok_core::url::route_url_signed(
///     "email.verify",
///     &[("id", "42")],
///     3600,        // expires in 1 hour (from now)
///     "my-secret", // signing key
/// );
/// // => "/email/verify/42?expires=1712345678&signature=abc123..."
/// ```
#[cfg(feature = "crypto")]
pub fn route_url_signed(
    name: &str,
    params: &[(&str, &str)],
    ttl_seconds: u64,
    secret: &str,
) -> String {
    let path = named_routes::resolve(name, params);
    let expires = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
        .saturating_add(ttl_seconds);

    let signer = crate::crypto::encrypt::Signer::new(secret);
    let data = format!("{}:{}", path, expires);
    let signature = signer.sign(&data);

    format!("{}?expires={}&signature={}", path, expires, signature)
}

/// Verify a signed URL generated by [`route_url_signed`].
///
/// Returns `true` if the signature is valid and the URL has not expired.
///
/// # Examples
///
/// ```rust,ignore
/// let valid = rok_core::url::verify_signed_url(
///     "/email/verify/42",
///     "1712345678",
///     "abc123...",
///     "my-secret",
/// );
/// ```
#[cfg(feature = "crypto")]
pub fn verify_signed_url(
    path: &str,
    expires: &str,
    signature: &str,
    secret: &str,
) -> bool {
    // Check expiration
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();

    let expires_secs: u64 = match expires.parse() {
        Ok(e) => e,
        Err(_) => return false,
    };

    if now > expires_secs {
        return false;
    }

    // Verify signature
    let data = format!("{}:{}", path, expires);
    let signer = crate::crypto::encrypt::Signer::new(secret);
    signer.verify(&data, signature)
}