uhhhh
This commit is contained in:
parent
2843aec125
commit
dea8968f6b
11 changed files with 276 additions and 39 deletions
43
Cargo.lock
generated
43
Cargo.lock
generated
|
@ -225,6 +225,19 @@ version = "1.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d1c9c15093eb224f0baa400f38fcd713fc1391a6f1c389d886beef146d60a3"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"blowfish",
|
||||
"getrandom",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
@ -249,6 +262,16 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blowfish"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.7.0"
|
||||
|
@ -307,6 +330,16 @@ dependencies = [
|
|||
"windows-targets",
|
||||
]
|
||||
|
||||
[[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 = "clap"
|
||||
version = "4.4.6"
|
||||
|
@ -857,6 +890,7 @@ name = "imgboard"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"bcrypt",
|
||||
"chrono",
|
||||
"clap",
|
||||
"dotenvy",
|
||||
|
@ -888,6 +922,15 @@ dependencies = [
|
|||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
|
|
|
@ -6,7 +6,8 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.6.20", features = ["tracing", "macros", "headers"] }
|
||||
axum = { version = "0.6.20", features = ["tracing", "macros", "headers", "form"] }
|
||||
bcrypt = "0.15.0"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
clap = { version = "4.4.6", features = ["derive", "env"] }
|
||||
dotenvy = "0.15.7"
|
||||
|
|
|
@ -20,9 +20,6 @@ use crate::config::Config;
|
|||
use crate::state::AppState;
|
||||
use crate::templates::handlebars;
|
||||
|
||||
use crate::pages::index::index;
|
||||
use crate::pages::user::{get_user, get_user_new};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
|
@ -52,9 +49,9 @@ async fn main() -> Result<()> {
|
|||
debug!("Building router");
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(index))
|
||||
.route("/users/:id", get(get_user))
|
||||
.route("/users/new", get(get_user_new))
|
||||
.route("/", get(pages::index))
|
||||
.route("/users/:id", get(pages::get_user))
|
||||
.route("/users/new", get(pages::get_user_new).post(pages::post_user_new))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state);
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub password: String,
|
||||
pub role: Role,
|
||||
|
||||
|
@ -12,7 +13,7 @@ pub struct User {
|
|||
pub last_active: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::Type, Debug)]
|
||||
#[derive(sqlx::Type, Debug, Clone, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum Role {
|
||||
Viewer = 1,
|
||||
|
@ -37,12 +38,27 @@ impl Serialize for Role {
|
|||
|
||||
impl From<i32> for Role {
|
||||
fn from(value: i32) -> Self {
|
||||
// From<i32> is only used for database mapping, so panicking on invalid values is okay
|
||||
match value {
|
||||
1 => Self::Viewer,
|
||||
2 => Self::Editor,
|
||||
3 => Self::Manager,
|
||||
4 => Self::Admin,
|
||||
_ => unreachable!("Role should never be outside 1-4")
|
||||
_ => unreachable!("Role should never be outside 1-4"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Role {
|
||||
type Error = eyre::Error;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
"viewer" => Ok(Self::Viewer),
|
||||
"editor" => Ok(Self::Editor),
|
||||
"manager" => Ok(Self::Manager),
|
||||
"admin" => Ok(Self::Admin),
|
||||
_ => Err(eyre::eyre!("Invalid role string value")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
pub mod index;
|
||||
pub mod user;
|
||||
mod index;
|
||||
mod user;
|
||||
|
||||
pub use index::index;
|
||||
pub use user::{get_user, get_user_new, post_user_new};
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
body::BoxBody,
|
||||
extract::{Path, State},
|
||||
response::IntoResponse,
|
||||
response::{IntoResponse, Response},
|
||||
Form,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use sqlx::{query, query_as};
|
||||
use tracing::error;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
extractor::ExtractUserToken,
|
||||
|
@ -25,47 +28,55 @@ pub async fn get_user(
|
|||
Ok(user) => user,
|
||||
Err(why) => {
|
||||
error!("Getting user: {}", why);
|
||||
return Page::new(
|
||||
state
|
||||
.hbs
|
||||
.render::<PageData>("error.hbs", &Default::default()),
|
||||
);
|
||||
return Page::new(state.hbs.render(
|
||||
"error.hbs",
|
||||
&PageData {
|
||||
message: Some("Internal server error".into()),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Page::new(
|
||||
state
|
||||
.hbs
|
||||
.render("error.hbs", &PageData { user: Some(user) }),
|
||||
)
|
||||
Page::new(state.hbs.render(
|
||||
"error.hbs",
|
||||
&PageData {
|
||||
user: Some(user),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_user_new(
|
||||
State(state): State<Arc<AppState>>,
|
||||
ExtractUserToken(user): ExtractUserToken,
|
||||
) -> impl IntoResponse {
|
||||
if user.is_some()
|
||||
&& match user.as_ref().unwrap().role {
|
||||
Role::Viewer => false,
|
||||
Role::Editor => false,
|
||||
Role::Manager => false,
|
||||
Role::Admin => true,
|
||||
// If there's already an authenticated user, check if they're an admin
|
||||
// Admins can create new users, others can't
|
||||
if let Some(user) = user {
|
||||
tracing::debug!("User is authenticated");
|
||||
|
||||
if user.role == Role::Admin {
|
||||
tracing::debug!("Authenticated user is admin");
|
||||
|
||||
return Page::new(
|
||||
state
|
||||
.hbs
|
||||
.render::<PageData>("new_user.hbs", &Default::default()),
|
||||
);
|
||||
}
|
||||
{
|
||||
// Admins can create accounts
|
||||
// Handlebars expects *some* value when rendering a template, so just pass an empty BTreeMap
|
||||
let map = BTreeMap::<&str, &str>::new();
|
||||
return Page::new(state.hbs.render("new_user.hbs", &map));
|
||||
} else if user.is_some() {
|
||||
// No permissions to create account
|
||||
|
||||
tracing::debug!("Authenticated user is not an admin");
|
||||
return Page::new(
|
||||
state
|
||||
.hbs
|
||||
.render::<PageData>("error.hbs", &Default::default()),
|
||||
);
|
||||
}
|
||||
// Else, let the user create a new account if no accounts exist yet
|
||||
|
||||
tracing::debug!("There is no authenticated user");
|
||||
|
||||
// Else, let the user create a new account, but only if no accounts exist yet
|
||||
let count = match query!(r#"SELECT COUNT(id) FROM users"#)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
|
@ -81,8 +92,145 @@ pub async fn get_user_new(
|
|||
}
|
||||
};
|
||||
|
||||
tracing::debug!("Current user count: {}", count);
|
||||
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert("count", count);
|
||||
|
||||
Page::new(state.hbs.render("new_user.hbs", &map))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UserNew {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub password2: String,
|
||||
}
|
||||
|
||||
pub async fn post_user_new(
|
||||
State(state): State<Arc<AppState>>,
|
||||
ExtractUserToken(existing_user): ExtractUserToken,
|
||||
Form(form_data): Form<UserNew>,
|
||||
) -> Response<BoxBody> {
|
||||
if existing_user.is_none() {
|
||||
let count = match query!(r#"SELECT COUNT(id) FROM users"#)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r.count.unwrap_or(0),
|
||||
Err(why) => {
|
||||
error!("Getting user count: {}", why);
|
||||
|
||||
return Page::new(state.hbs.render(
|
||||
"error.hbs",
|
||||
&PageData {
|
||||
message: Some("Internal server error".into()),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if count != 0 {
|
||||
info!("");
|
||||
|
||||
return Page::new(state.hbs.render(
|
||||
"error.hbs",
|
||||
&PageData {
|
||||
message: Some("You need to be logged in to create new users".into()),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
} else if let Some(user) = existing_user.clone() {
|
||||
if user.role != Role::Admin {
|
||||
info!(
|
||||
user_id = user.id,
|
||||
"User tried to access POST /users/new despite not being admin"
|
||||
);
|
||||
|
||||
return Page::new(state.hbs.render(
|
||||
"error.hbs",
|
||||
&PageData {
|
||||
message: Some("Only admins can create new users".into()),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
if form_data.password != form_data.password2 {
|
||||
return Page::new(state.hbs.render(
|
||||
"new_user.hbs",
|
||||
&PageData {
|
||||
message: Some("Passwords don't match".into()),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let hashed_password = match bcrypt::hash(form_data.password, 12) {
|
||||
Ok(hash) => hash,
|
||||
Err(why) => {
|
||||
error!("Hashing password: {}", why);
|
||||
|
||||
return Page::new(state.hbs.render(
|
||||
"error.hbs",
|
||||
&PageData {
|
||||
message: Some("Internal server error".into()),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let user = match query_as!(
|
||||
User,
|
||||
r#"INSERT INTO users (username, password) VALUES ($1, $2) RETURNING *"#,
|
||||
form_data.username,
|
||||
hashed_password,
|
||||
)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(u) => u,
|
||||
Err(why) => {
|
||||
error!("Creating user: {}", why);
|
||||
|
||||
return Page::new(state.hbs.render(
|
||||
"error.hbs",
|
||||
&PageData {
|
||||
message: Some("Internal server error".into()),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if existing_user.is_some() {
|
||||
// Render template with new user info (ID, username)
|
||||
|
||||
return Page::new(state.hbs.render(
|
||||
"new_user_confirm.hbs",
|
||||
&PageData {
|
||||
user: existing_user,
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
|
||||
Page::new(state.hbs.render(
|
||||
"new_user.hbs",
|
||||
&PageData {
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
.into_response()
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::{extract::{FromRef, FromRequestParts}, async_trait, http::request::Parts};
|
||||
use sqlx::{postgres::Postgres, Pool};
|
||||
|
||||
|
@ -19,3 +21,9 @@ where
|
|||
Ok(Self::from_ref(state))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<Arc<AppState>> for AppState {
|
||||
fn from_ref(input: &Arc<AppState>) -> Self {
|
||||
(**input).clone()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,4 +54,7 @@ impl IntoResponse for Page {
|
|||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct PageData {
|
||||
pub user: Option<User>,
|
||||
|
||||
// An optional flash message. Not all templates will handle this.
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{{#*inline "page"}}
|
||||
<h1>Error</h1>
|
||||
<p>An internal error occurred. Sorry :(</p>
|
||||
<p>Error: {{message}}</p>
|
||||
{{/inline}}
|
||||
{{> root.hbs}}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
{{#*inline "page"}}
|
||||
<h1>New user</h1>
|
||||
{{#if message}}
|
||||
<div>
|
||||
{{message}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<form method="POST">
|
||||
<label>Username <input type="text" name="username" /></label>
|
||||
<label>Password <input type="password" name="password" /></label>
|
||||
|
|
13
templates/new_user_confirm.hbs
Normal file
13
templates/new_user_confirm.hbs
Normal file
|
@ -0,0 +1,13 @@
|
|||
{{#*inline "page"}}
|
||||
<h1>New user created!</h1>
|
||||
{{#if message}}
|
||||
<div>
|
||||
{{message}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<ul>
|
||||
<li>ID: {{new_user.id}}</li>
|
||||
<li>Username: {{new_user.username}}</li>
|
||||
</ul>
|
||||
{{/inline}}
|
||||
{{> root.hbs}}
|
Loading…
Reference in a new issue