compiler error I guess?

This commit is contained in:
Daniel Flanagan 2023-11-13 16:16:17 -06:00
parent 10d48a91c3
commit 165d8bb729
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
12 changed files with 536 additions and 37 deletions

283
Cargo.lock generated
View file

@ -17,6 +17,41 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aes-gcm"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]]
name = "aho-corasick"
version = "1.1.2"
@ -32,6 +67,18 @@ version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "argon2"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9"
dependencies = [
"base64ct",
"blake2",
"cpufeatures",
"password-hash",
]
[[package]]
name = "async-trait"
version = "0.1.74"
@ -99,6 +146,26 @@ dependencies = [
"tower-service",
]
[[package]]
name = "axum_csrf"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c0ccdf64283e19f1ee0c4180f216ba28eedef4696dc3ee947e1dc01b3486960"
dependencies = [
"async-trait",
"axum-core",
"base64ct",
"cookie",
"hmac",
"http",
"rand",
"sha2",
"thiserror",
"time",
"tower-layer",
"tower-service",
]
[[package]]
name = "backtrace"
version = "0.3.69"
@ -120,6 +187,12 @@ version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -132,6 +205,15 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -162,6 +244,33 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "cookie"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8"
dependencies = [
"aes-gcm",
"base64",
"hmac",
"percent-encoding",
"rand",
"sha2",
"subtle",
"time",
"version_check",
]
[[package]]
name = "cpufeatures"
version = "0.2.11"
@ -197,9 +306,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"rand_core",
"typenum",
]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]]
name = "deadpool"
version = "0.10.0"
@ -261,6 +380,7 @@ dependencies = [
"diesel_derives",
"libsqlite3-sys",
"time",
"uuid",
]
[[package]]
@ -303,6 +423,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
@ -396,6 +517,27 @@ dependencies = [
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "ghash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]]
name = "gimli"
version = "0.28.0"
@ -438,6 +580,15 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "http"
version = "0.2.10"
@ -531,6 +682,15 @@ dependencies = [
"libc",
]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]]
name = "itoa"
version = "1.0.9"
@ -600,7 +760,11 @@ name = "lyrs"
version = "0.1.0"
dependencies = [
"anyhow",
"argon2",
"axum",
"axum_csrf",
"base64",
"cookie",
"deadpool",
"deadpool-diesel",
"diesel",
@ -608,6 +772,7 @@ dependencies = [
"maud",
"notify",
"serde",
"thiserror",
"tokio",
"tower-http",
"tower-livereload",
@ -770,6 +935,12 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "overload"
version = "0.1.1"
@ -799,6 +970,17 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]]
name = "percent-encoding"
version = "2.3.0"
@ -843,12 +1025,30 @@ version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "polyval"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@ -891,6 +1091,36 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.3.5"
@ -1059,6 +1289,17 @@ dependencies = [
"digest",
]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@ -1103,6 +1344,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "subtle"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
version = "1.0.109"
@ -1131,6 +1378,26 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "thiserror"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
]
[[package]]
name = "thread_local"
version = "1.1.7"
@ -1403,6 +1670,22 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "uuid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
[[package]]
name = "valuable"
version = "0.1.0"

View file

@ -5,16 +5,30 @@ edition = "2021"
[dependencies]
anyhow = "1.0.75"
# web
axum = { version = "0.6.20", features = ["headers"] }
deadpool = "0.10.0"
deadpool-diesel = { version = "0.5.0", features = ["rt_tokio_1", "sqlite", "tracing", "serde"] }
diesel = { version = "2.1.3", features = ["sqlite"] }
diesel_migrations = { version = "2.1.0", features = ["sqlite"] }
axum_csrf = { version = "0.8.0", features = ["layer"] }
base64 = "0.21.5"
cookie = "0.18.0"
maud = "0.25.0"
notify = "6.1.1"
serde = { version = "1.0.192", features = ["derive"] }
tokio = { version = "1.34.0", features = ["full", "macros", "rt-multi-thread"] }
tower-http = { version = "0.4.4", features = ["fs"] }
tower-livereload = "0.8.2"
# instrumentation
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
# database
deadpool = "0.10.0"
deadpool-diesel = { version = "0.5.0", features = ["rt_tokio_1", "sqlite", "tracing", "serde"] }
diesel = { version = "2.1.3", features = ["sqlite", "uuid"] }
diesel_migrations = { version = "2.1.0", features = ["sqlite"] }
# fancy during-development stuff
notify = "6.1.1"
tower-livereload = "0.8.2"
argon2 = { version = "0.5.2", features = ["std"] }
thiserror = "1.0.50"

View file

@ -98,6 +98,7 @@ textarea:not([rows]) {
:root {
--font: monospace;
--br: 3px;
/* catppuccin latte */
--Rosewater: #dc8a78;
@ -171,6 +172,32 @@ ol {
padding-left: 2ex;
}
input {
padding: 1ex 2ex;
background-color: var(--Surface0);
border: solid 1px var(--Surface2);
border-radius: var(--br);
color: inherit;
}
input:hover {
border: solid 1px var(--Overlay0);
/* background-color: var(--Surface0); */
}
button {
cursor: pointer;
padding: 1ex 2ex;
background-color: var(--Surface0);
border: 0;
border-radius: var(--br);
color: inherit;
}
button:hover {
background-color: var(--Surface1);
}
html {
background-color: var(--bg);
color: var(--text);
@ -197,7 +224,7 @@ body>header a:not([class]) {
display: flex;
align-items: center;
padding: 1ex 2ex;
transition: color 0.1s ease-out, background-color 0.1s ease-out;
/* transition: color 0.1s ease-out, background-color 0.1s ease-out; */
text-decoration: none;
color: var(--primary);
gap: 0.5ex;
@ -214,7 +241,7 @@ main.prose {
text-decoration: none;
color: currentColor;
box-shadow: 0 -1px 1px var(--Mantle);
border-radius: 2px;
border-radius: var(--br);
}
body>header a:hover {
@ -225,4 +252,16 @@ body>header a:hover {
.bg-primary {
background-color: var(--primary);
color: var(--bg);
}
form {
display: flex;
flex-direction: column;
gap: 2ex;
}
form>label {
display: flex;
flex-direction: column;
gap: 0.5ex;
}

View file

@ -59,6 +59,7 @@
RUST_SRC_PATH = rustPlatform.rustLibSrc;
shellHook = ''
export DATABASE_URL="sqlite://./data/lyrs.sqlitedb";
export COOKIE_KEY="2z49_8yfKUkoTOo0cjzzjwufCfhKvfOIc1CGleuTXC5zRqY4U0Xhkd34ipREQN5iHRH62tt5O7y6U5mmFBH3MA"
'';
};
});

View file

@ -1,5 +1,7 @@
create table users (
id text primary key,
username text
name text,
hashed_password binary
)
password_digest binary
);
create unique index users_username on users(username);

25
src/error.rs Normal file
View file

@ -0,0 +1,25 @@
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
pub struct AppError(anyhow::Error);
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0),
)
.into_response()
}
}
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}

View file

@ -5,8 +5,10 @@
mod app;
mod database;
mod error;
mod feather_icons;
mod instrumentation;
mod models;
mod partials;
mod router;
mod schema;

23
src/models.rs Normal file
View file

@ -0,0 +1,23 @@
use diesel::prelude::*;
#[derive(Queryable, Selectable)]
#[diesel(table_name = crate::schema::users)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct User {
// pub id: Text,
// pub username: sql_types::Text,
// pub name: sql_types::Text,
// pub password_digest: sql_types::Binary,
pub id: Vec<u8>,
pub username: String,
pub name: Option<String>,
pub password_digest: Vec<u8>,
}
#[derive(Insertable)]
#[diesel(table_name = crate::schema::users)]
pub struct NewUser<'a> {
pub username: &'a str,
pub name: Option<&'a str>,
pub password_digest: &'a [u8],
}

View file

@ -1,12 +1,26 @@
use std::{path::Path, sync::Arc};
use std::{env, path::Path, sync::Arc};
use axum::{http::StatusCode, routing::get, Router};
use argon2::{
password_hash::{rand_core::OsRng, SaltString},
Argon2, PasswordHasher,
};
use axum::{
http::StatusCode,
response::{Html, IntoResponse, Response},
routing::get,
Form, Router,
};
use axum_csrf::{CsrfConfig, CsrfLayer, CsrfToken};
use maud::html;
use notify::Watcher;
use serde::Deserialize;
use tower_http::services::ServeDir;
use tower_livereload::LiveReloadLayer;
use tracing::instrument;
use crate::{state::State, views};
use base64::prelude::*;
use crate::{error::AppError, models::NewUser, state::State, views};
#[instrument]
pub async fn new() -> Result<Router, anyhow::Error> {
@ -23,14 +37,73 @@ pub async fn new() -> Result<Router, anyhow::Error> {
let mut watcher = notify::recommended_watcher(move |_| reloader.reload())?;
watcher.watch(Path::new("assets"), notify::RecursiveMode::Recursive)?;
let cookie_key_bytes: Vec<u8> = BASE64_URL_SAFE_NO_PAD
.decode(env::var("COOKIE_KEY").expect("COOKIE_KEY not set"))
.expect("COOKIE_KEY not base64 URL-safe encoded");
let cookie_key = cookie::Key::from(&cookie_key_bytes);
let csrf_config = CsrfConfig::default().with_key(Some(cookie_key));
let router = Router::new()
.fallback(|| async { (StatusCode::NOT_FOUND, "404 page not found") })
.nest("/app", app_router)
.nest_service("/assets", assets_dir)
.route("/", get(views::index))
.route("/register", get(views::register))
.route("/register", get(views::register).post(register))
.with_state(state)
.layer(CsrfLayer::new(csrf_config))
.layer(live_reload_layer);
Ok(router)
}
#[derive(Deserialize)]
struct Register {
authenticity_token: String,
username: String,
password: String,
}
impl<'a> TryInto<NewUser<'a>> for Register {
type Error = ();
fn try_into(self) -> Result<NewUser<'a>, Self::Error> {
todo!()
}
}
async fn register(
csrf_token: CsrfToken,
Form(register): Form<Register>,
) -> Result<impl IntoResponse, AppError> {
// TODO: https://docs.rs/axum_csrf/latest/axum_csrf/#prevent-post-replay-attacks-with-csrf
let v = csrf_token.verify(&register.authenticity_token);
println!("{:?} {:?}", register.authenticity_token, v);
if v.is_err() {
return Ok((
StatusCode::BAD_REQUEST,
Html(html! { "invalid request" }.into_string()),
));
}
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_digest = argon2.hash_password(register.password.as_bytes(), &salt)?;
let new_user = NewUser {
username: &register.username,
name: None,
password_digest: password_digest.to_string().as_bytes(),
};
Ok((
StatusCode::CREATED,
Html(
html! {
h1 { (register.username) }
h1 { (register.password) }
}
.into_string(),
),
))
}

View file

@ -2,8 +2,9 @@
diesel::table! {
users (id) {
id -> Nullable<Text>,
id -> Binary,
username -> Text,
name -> Nullable<Text>,
hashed_password -> Nullable<Binary>,
password_digest -> Binary,
}
}

View file

@ -10,6 +10,7 @@ impl State {
pub async fn new() -> Result<Self, anyhow::Error> {
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let database = database::Database::new(database_url).await?;
Ok(State { database })
}
}

View file

@ -1,4 +1,5 @@
use axum::response::Html;
use axum::response::{Html, IntoResponse};
use axum_csrf::CsrfToken;
use maud::html;
use tracing::instrument;
@ -21,7 +22,7 @@ pub async fn index() -> Html<String> {
li { "Lightweight and fast" }
}
section class="flex gap" {
button hx-get="/register" hx-target="body>main" hx-push-url="true" class="button bg-primary" { "Try now" }
a href="/register" class="button bg-primary" { "Try now" }
a class="button" href="/login" { "Login" }
}
}
@ -31,27 +32,61 @@ pub async fn index() -> Html<String> {
)
}
#[instrument]
pub async fn register() -> Html<String> {
Html(
html! {
h1 { "Register an account" }
p { "Stop struggling to share the same messy set of PowerPoint files or Google Presentations. Make editing and controlling your live lyrics and music displays easy and simple." }
ul {
li { "Live collaboration with your team" }
li { "Fully compatible with any device" }
li { "Simple workflow" }
li { "Dark theme and light theme" }
li { "Generous free plan" }
li { "Lightweight and fast" }
pub async fn register(csrf: CsrfToken) -> impl IntoResponse {
let token = csrf.authenticity_token().unwrap();
(
csrf,
Html(
html! {
(header())
main class="prose" {
h1 { "Register an account" }
form method="post" {
input type="hidden" name="authenticity_token" value=(token) {}
label {
"Username:"
input name="username" {}
}
label {
"Password:"
input type="password" name="password" {}
}
button type="submit" { "Create Account" }
}
}
(footer())
}
section class="flex gap" {
a class="button bg-primary" href="/register/anonymous" { "Try now" }
a class="button" href="/login" { "Login" }
}
}
.into_string(),
.into_string(),
),
)
.into_response()
}
pub async fn login(csrf: CsrfToken) -> impl IntoResponse {
let token = csrf.authenticity_token().unwrap();
(
csrf,
Html(
html! {
(header())
main class="prose" {
h1 { "Login" }
form method="post" {
input type="hidden" name="authenticity_token" value=(token) {}
label {
input {}
}
label {
input {}
}
}
}
(footer())
}
.into_string(),
),
)
.into_response()
}
#[instrument]