Bloody beans, there's finally a workig database
This commit is contained in:
parent
22761ee92e
commit
61e3a2cc31
18 changed files with 1943 additions and 106 deletions
|
@ -1,2 +1,3 @@
|
||||||
[env]
|
[env]
|
||||||
RUST_BACKTRACE = "1"
|
RUST_BACKTRACE = "1"
|
||||||
|
RUSTFLAGS = "--cfg uuid_unstable"
|
||||||
|
|
1723
Cargo.lock
generated
1723
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -28,7 +28,8 @@ thiserror = "1.0.50"
|
||||||
axum-macros = "0.3.8"
|
axum-macros = "0.3.8"
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
|
|
||||||
# irust
|
# db
|
||||||
# bacon
|
sea-orm = { version = "0.12.6", features = ["sqlx-sqlite", "macros", "runtime-tokio-rustls"] }
|
||||||
# sqlx (sea orm?)
|
sea-orm-migration = { version = "0.12.6", features = ["sqlx-sqlite"] }
|
||||||
# poem-openapi?
|
uuid = { version = "1.5.0", features = ["v7", "atomic", "fast-rng", "macro-diagnostics"] }
|
||||||
|
password-hash = "0.5.0"
|
||||||
|
|
|
@ -53,12 +53,19 @@
|
||||||
rust-analyzer
|
rust-analyzer
|
||||||
nodePackages_latest.vscode-langservers-extracted
|
nodePackages_latest.vscode-langservers-extracted
|
||||||
|
|
||||||
|
# to install sea-orm-cli
|
||||||
|
pkg-config
|
||||||
|
openssl
|
||||||
|
|
||||||
hurl
|
hurl
|
||||||
];
|
];
|
||||||
RUST_SRC_PATH = rustPlatform.rustLibSrc;
|
RUST_SRC_PATH = rustPlatform.rustLibSrc;
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export DATABASE_URL="sqlite://./data/lyrs.sqlitedb";
|
export MIGRATION_DIR="src/migrator"
|
||||||
|
export DATABASE_URL="sqlite://./data/lyrs.sqlitedb?mode=rwc";
|
||||||
export COOKIE_KEY="2z49_8yfKUkoTOo0cjzzjwufCfhKvfOIc1CGleuTXC5zRqY4U0Xhkd34ipREQN5iHRH62tt5O7y6U5mmFBH3MA"
|
export COOKIE_KEY="2z49_8yfKUkoTOo0cjzzjwufCfhKvfOIc1CGleuTXC5zRqY4U0Xhkd34ipREQN5iHRH62tt5O7y6U5mmFBH3MA"
|
||||||
|
export RUST_BACKTRACE="1"
|
||||||
|
export RUSTFLAGS="--cfg uuid_unstable"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "nightly"
|
channel = "1.73"
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
5
src/entities/mod.rs
Normal file
5
src/entities/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6
|
||||||
|
|
||||||
|
pub mod prelude;
|
||||||
|
|
||||||
|
pub mod user;
|
3
src/entities/prelude.rs
Normal file
3
src/entities/prelude.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6
|
||||||
|
|
||||||
|
pub use super::user::Entity as User;
|
22
src/entities/user.rs
Normal file
22
src/entities/user.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.6
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
#[sea_orm(table_name = "user")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(
|
||||||
|
primary_key,
|
||||||
|
auto_increment = false,
|
||||||
|
column_type = "Binary(BlobSize::Blob(None))"
|
||||||
|
)]
|
||||||
|
pub id: Vec<u8>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub username: String,
|
||||||
|
pub password_digest: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -1,13 +1,14 @@
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
|
use core::fmt;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum AppError {
|
pub enum AppError {
|
||||||
|
Database(#[from] sea_orm::error::DbErr),
|
||||||
|
PasswordHash(#[from] password_hash::Error),
|
||||||
InvalidCsrf(#[from] axum_csrf::CsrfError),
|
InvalidCsrf(#[from] axum_csrf::CsrfError),
|
||||||
Other(#[from] anyhow::Error),
|
Other(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
@ -17,6 +18,8 @@ impl AppError {
|
||||||
match self {
|
match self {
|
||||||
AppError::Other(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
AppError::Other(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
AppError::InvalidCsrf(_) => StatusCode::BAD_REQUEST,
|
AppError::InvalidCsrf(_) => StatusCode::BAD_REQUEST,
|
||||||
|
AppError::PasswordHash(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
AppError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +27,8 @@ impl AppError {
|
||||||
match self {
|
match self {
|
||||||
AppError::Other(e) => format!("something went wrong: {}", e),
|
AppError::Other(e) => format!("something went wrong: {}", e),
|
||||||
AppError::InvalidCsrf(e) => format!("unable to verify csrf: {}", e),
|
AppError::InvalidCsrf(e) => format!("unable to verify csrf: {}", e),
|
||||||
|
AppError::PasswordHash(e) => format!("failed to hash password: {}", e),
|
||||||
|
AppError::Database(e) => format!("database error: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
use tracing::{instrument, trace};
|
use tracing::{info, instrument};
|
||||||
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
|
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
pub fn init() {
|
pub fn init() {
|
||||||
color_eyre::install().expect("Failed to install color_eyre");
|
color_eyre::install().expect("Failed to install color_eyre");
|
||||||
setup_trace_logger();
|
setup_trace_logger();
|
||||||
|
info!("Instrumentation initialized.");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
|
@ -13,6 +15,4 @@ pub fn setup_trace_logger() {
|
||||||
.parse_lossy("info,lyrs=trace");
|
.parse_lossy("info,lyrs=trace");
|
||||||
|
|
||||||
tracing_subscriber::fmt().with_env_filter(filter).init();
|
tracing_subscriber::fmt().with_env_filter(filter).init();
|
||||||
|
|
||||||
trace!("Starting...");
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
// TODO: Implement authn
|
// TODO: Implement authn
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod database;
|
mod entities;
|
||||||
mod error;
|
mod error;
|
||||||
mod feather_icons;
|
mod feather_icons;
|
||||||
mod instrumentation;
|
mod instrumentation;
|
||||||
mod models;
|
mod migrator;
|
||||||
mod partials;
|
mod partials;
|
||||||
mod router;
|
mod router;
|
||||||
mod server;
|
mod server;
|
||||||
|
|
12
src/migrator.rs
Normal file
12
src/migrator.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
pub struct Migrator;
|
||||||
|
|
||||||
|
mod m20231114_143300_init;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigratorTrait for Migrator {
|
||||||
|
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||||
|
vec![Box::new(m20231114_143300_init::Migration)]
|
||||||
|
}
|
||||||
|
}
|
52
src/migrator/m20231114_143300_init.rs
Normal file
52
src/migrator/m20231114_143300_init.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
impl MigrationName for Migration {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"m20231114_143300_init.rs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(User::Table)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(User::Id)
|
||||||
|
.binary_len(16)
|
||||||
|
.not_null()
|
||||||
|
.primary_key(),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(User::Name).string())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(User::Username)
|
||||||
|
.string()
|
||||||
|
.not_null()
|
||||||
|
.unique_key(),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(User::PasswordDigest).text().not_null())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define how to rollback this migration: Drop the Bakery table.
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(User::Table).to_owned())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Iden)]
|
||||||
|
pub enum User {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Name,
|
||||||
|
Username,
|
||||||
|
PasswordDigest,
|
||||||
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
pub struct User {
|
|
||||||
pub id: Vec<u8>,
|
|
||||||
pub username: String,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub password_digest: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct NewUser<'a> {
|
|
||||||
pub username: &'a str,
|
|
||||||
pub name: Option<&'a str>,
|
|
||||||
pub password_digest: Vec<u8>,
|
|
||||||
}
|
|
116
src/router.rs
116
src/router.rs
|
@ -1,26 +1,22 @@
|
||||||
use std::{env, path::Path};
|
use crate::entities::{prelude::*, *};
|
||||||
|
use crate::state;
|
||||||
use argon2::{
|
use crate::{error::AppError, views};
|
||||||
password_hash::{rand_core::OsRng, SaltString},
|
use argon2::password_hash::rand_core::OsRng;
|
||||||
Argon2, PasswordHasher,
|
use argon2::password_hash::SaltString;
|
||||||
};
|
use argon2::{Argon2, PasswordHasher};
|
||||||
use axum::{
|
use axum::extract::State;
|
||||||
http::StatusCode,
|
use axum::{http::StatusCode, response::Html, routing::get, Form, Router};
|
||||||
response::{Html, IntoResponse},
|
|
||||||
routing::get,
|
|
||||||
Form, Router,
|
|
||||||
};
|
|
||||||
use axum_csrf::{CsrfConfig, CsrfLayer, CsrfToken};
|
use axum_csrf::{CsrfConfig, CsrfLayer, CsrfToken};
|
||||||
|
use base64::prelude::*;
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use notify::Watcher;
|
use notify::Watcher;
|
||||||
|
use sea_orm::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::{env, path::Path};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tower_livereload::LiveReloadLayer;
|
use tower_livereload::LiveReloadLayer;
|
||||||
use tracing::instrument;
|
use tracing::{info, instrument};
|
||||||
|
|
||||||
use base64::prelude::*;
|
|
||||||
|
|
||||||
use crate::{error::AppError, models::NewUser, state::State, views};
|
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
pub async fn new() -> Result<Router, anyhow::Error> {
|
pub async fn new() -> Result<Router, anyhow::Error> {
|
||||||
|
@ -29,7 +25,7 @@ pub async fn new() -> Result<Router, anyhow::Error> {
|
||||||
.route("/hello-world-text", get(views::greet_world_text));
|
.route("/hello-world-text", get(views::greet_world_text));
|
||||||
|
|
||||||
let assets_dir = ServeDir::new("./assets");
|
let assets_dir = ServeDir::new("./assets");
|
||||||
let state = State::new().await?;
|
let state = state::State::new().await?;
|
||||||
|
|
||||||
let live_reload_layer = LiveReloadLayer::new();
|
let live_reload_layer = LiveReloadLayer::new();
|
||||||
let reloader = live_reload_layer.reloader();
|
let reloader = live_reload_layer.reloader();
|
||||||
|
@ -58,6 +54,14 @@ pub async fn new() -> Result<Router, anyhow::Error> {
|
||||||
Ok(router)
|
Ok(router)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn csrf_verify(c: CsrfToken, t: &str) -> Result<(), AppError> {
|
||||||
|
// TODO: https://docs.rs/axum_csrf/latest/axum_csrf/#prevent-post-replay-attacks-with-csrf
|
||||||
|
c.verify(t)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppRes = Result<(StatusCode, Html<String>), AppError>;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Register {
|
struct Register {
|
||||||
authenticity_token: String,
|
authenticity_token: String,
|
||||||
|
@ -65,40 +69,52 @@ struct Register {
|
||||||
password: String,
|
password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TryInto<NewUser<'a>> for &'a Register {
|
impl TryInto<user::ActiveModel> for Register {
|
||||||
type Error = anyhow::Error;
|
type Error = AppError;
|
||||||
|
|
||||||
fn try_into(self: &'a Register) -> Result<NewUser<'a>, Self::Error> {
|
fn try_into(self) -> Result<user::ActiveModel, Self::Error> {
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
Ok(user::ActiveModel {
|
||||||
let argon2 = Argon2::default();
|
id: ActiveValue::Set(uuid::Uuid::now_v7().into()),
|
||||||
let password_digest: Vec<u8> = argon2
|
name: ActiveValue::Set(None),
|
||||||
.hash_password(self.password.as_bytes(), &salt)?
|
username: ActiveValue::Set(self.username),
|
||||||
.hash
|
password_digest: ActiveValue::Set(password_digest(self.password)?),
|
||||||
.expect("no password hash")
|
|
||||||
.as_bytes()
|
|
||||||
.into();
|
|
||||||
Ok(NewUser {
|
|
||||||
username: &self.username,
|
|
||||||
name: None,
|
|
||||||
password_digest,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn password_digest<S>(s: S) -> Result<String, password_hash::Error>
|
||||||
|
where
|
||||||
|
S: AsRef<str>,
|
||||||
|
{
|
||||||
|
Ok(Argon2::default()
|
||||||
|
.hash_password(s.as_ref().as_bytes(), &SaltString::generate(&mut OsRng))?
|
||||||
|
.serialize()
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl user::ActiveModel {}
|
||||||
|
|
||||||
async fn register(
|
async fn register(
|
||||||
c: CsrfToken,
|
c: CsrfToken,
|
||||||
Form(register): Form<Register>,
|
State(s): State<Arc<state::State>>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
Form(f): Form<Register>,
|
||||||
// TODO: https://docs.rs/axum_csrf/latest/axum_csrf/#prevent-post-replay-attacks-with-csrf
|
) -> AppRes {
|
||||||
c.verify(®ister.authenticity_token)?;
|
csrf_verify(c, &f.authenticity_token)?;
|
||||||
|
|
||||||
let new_user: NewUser = (®ister).try_into()?;
|
// TODO: handle duplicate username
|
||||||
|
|
||||||
|
let new: user::ActiveModel = f.try_into()?;
|
||||||
|
let res = User::insert(new).exec(&s.db).await;
|
||||||
|
|
||||||
|
info!("insert new user result: {:?}", res);
|
||||||
|
|
||||||
|
res.map_err(anyhow::Error::from)?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
Html(
|
Html(
|
||||||
html! {
|
html! {
|
||||||
h1 { (new_user.username) }
|
// h1 { (new_user.username) }
|
||||||
}
|
}
|
||||||
.into_string(),
|
.into_string(),
|
||||||
),
|
),
|
||||||
|
@ -112,27 +128,15 @@ struct Login {
|
||||||
password: String,
|
password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn login(
|
async fn login(c: CsrfToken, Form(f): Form<Login>) -> AppRes {
|
||||||
csrf_token: CsrfToken,
|
csrf_verify(c, &f.authenticity_token)?;
|
||||||
Form(register): Form<Login>,
|
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
|
||||||
// TODO: https://docs.rs/axum_csrf/latest/axum_csrf/#prevent-post-replay-attacks-with-csrf
|
|
||||||
|
|
||||||
let v = csrf_token.verify(®ister.authenticity_token);
|
|
||||||
println!("{:?} {:?}", register.authenticity_token, v);
|
|
||||||
if v.is_err() {
|
|
||||||
return Ok((
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
Html(html! { "invalid request" }.into_string()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::CREATED,
|
StatusCode::OK,
|
||||||
Html(
|
Html(
|
||||||
html! {
|
html! {
|
||||||
h1 { (register.username) }
|
h1 { (f.username) }
|
||||||
h1 { (register.password) }
|
h1 { (f.password) }
|
||||||
}
|
}
|
||||||
.into_string(),
|
.into_string(),
|
||||||
),
|
),
|
||||||
|
|
21
src/state.rs
21
src/state.rs
|
@ -1,12 +1,21 @@
|
||||||
use std::env;
|
use std::{env, sync::Arc};
|
||||||
|
|
||||||
#[derive(Clone)]
|
use sea_orm::{Database, DatabaseConnection};
|
||||||
pub struct State {}
|
use sea_orm_migration::MigratorTrait;
|
||||||
|
|
||||||
|
use crate::migrator::Migrator;
|
||||||
|
|
||||||
|
pub struct State {
|
||||||
|
pub db: DatabaseConnection,
|
||||||
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub async fn new() -> Result<Self, anyhow::Error> {
|
pub async fn new() -> Result<Arc<Self>, anyhow::Error> {
|
||||||
let _database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||||
|
|
||||||
Ok(State {})
|
let db = Database::connect(database_url).await?;
|
||||||
|
Migrator::refresh(&db).await?;
|
||||||
|
|
||||||
|
Ok(Arc::new(State { db }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
30
src/views.rs
30
src/views.rs
|
@ -1,17 +1,20 @@
|
||||||
|
use crate::entities::{prelude::*, *};
|
||||||
|
use crate::{
|
||||||
|
error::AppError,
|
||||||
|
partials::{footer, header},
|
||||||
|
state,
|
||||||
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::State,
|
extract::State,
|
||||||
|
http::StatusCode,
|
||||||
response::{Html, IntoResponse},
|
response::{Html, IntoResponse},
|
||||||
};
|
};
|
||||||
use axum_csrf::CsrfToken;
|
use axum_csrf::CsrfToken;
|
||||||
use maud::html;
|
use maud::html;
|
||||||
|
use sea_orm::EntityTrait;
|
||||||
|
use std::sync::Arc;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
error::AppError,
|
|
||||||
models::User,
|
|
||||||
partials::{footer, header},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn csrf<F>(csrf: CsrfToken, cb: F) -> impl IntoResponse
|
pub async fn csrf<F>(csrf: CsrfToken, cb: F) -> impl IntoResponse
|
||||||
where
|
where
|
||||||
F: Fn(&str) -> Html<String>,
|
F: Fn(&str) -> Html<String>,
|
||||||
|
@ -20,6 +23,8 @@ where
|
||||||
(csrf, cb(&token))
|
(csrf, cb(&token))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppRes = Result<(StatusCode, Html<String>), AppError>;
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
pub async fn index() -> Html<String> {
|
pub async fn index() -> Html<String> {
|
||||||
Html(
|
Html(
|
||||||
|
@ -100,11 +105,8 @@ pub async fn login(t: CsrfToken) -> impl IntoResponse {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unreachable_code)]
|
pub async fn all_users(State(s): State<Arc<state::State>>) -> Result<Html<String>, AppError> {
|
||||||
pub async fn all_users(
|
let users: Vec<user::Model> = User::find().all(&s.db).await?;
|
||||||
State(_state): State<crate::state::State>,
|
|
||||||
) -> Result<Html<String>, AppError> {
|
|
||||||
let all_users: Vec<User> = vec![];
|
|
||||||
|
|
||||||
// @if let Some(name) = u.name {
|
// @if let Some(name) = u.name {
|
||||||
// name
|
// name
|
||||||
|
@ -117,7 +119,10 @@ pub async fn all_users(
|
||||||
main class="prose" {
|
main class="prose" {
|
||||||
h1 { "Users" }
|
h1 { "Users" }
|
||||||
ul {
|
ul {
|
||||||
@for u in all_users {
|
@if users.len() < 1 {
|
||||||
|
li { "It looks like there are no users yet!" }
|
||||||
|
} else {
|
||||||
|
@for u in users {
|
||||||
li {
|
li {
|
||||||
(u.username)
|
(u.username)
|
||||||
@if let Some(name) = u.name {
|
@if let Some(name) = u.name {
|
||||||
|
@ -129,6 +134,7 @@ pub async fn all_users(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
(footer())
|
(footer())
|
||||||
}
|
}
|
||||||
.into_string(),
|
.into_string(),
|
||||||
|
|
Loading…
Reference in a new issue