From c5c884f069d218ebcaf197a823b248167ba0f1de Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 23 Oct 2023 01:53:05 +0200 Subject: [PATCH 1/2] consistently name Err values `err` --- src/extractor.rs | 8 +++++--- src/pages/user/mod.rs | 20 ++++++++++---------- src/templates.rs | 4 ++-- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/extractor.rs b/src/extractor.rs index 63daeab..b037f5c 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -11,6 +11,8 @@ use crate::{model::user::User, state::AppState, token::Claims}; pub struct ExtractUserToken(pub Option); +pub const TOKEN_COOKIE_NAME: &'static str = "imgboard-token"; + #[async_trait] impl FromRequestParts for ExtractUserToken where @@ -22,8 +24,8 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let state = match parts.extract_with_state::(state).await { Ok(s) => s, - Err(why) => { - error!("Getting state: {}", why); + Err(err) => { + error!("Getting state: {}", err); return Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")); } }; @@ -36,7 +38,7 @@ where } }; - match cookie.get("imgboard-token") { + match cookie.get(TOKEN_COOKIE_NAME) { Some(token) => { let claims = Claims::decode(token, &state.decoding_key).map_err(|e| { error!("Decoding token claims: {}", e); diff --git a/src/pages/user/mod.rs b/src/pages/user/mod.rs index 4ff147d..ef1cc2f 100644 --- a/src/pages/user/mod.rs +++ b/src/pages/user/mod.rs @@ -26,8 +26,8 @@ pub async fn get_user( .await { Ok(user) => user, - Err(why) => { - error!("Getting user: {}", why); + Err(err) => { + error!("Getting user: {}", err); return Page::new(state.hbs.render( "error.hbs", &PageData { @@ -82,8 +82,8 @@ pub async fn get_user_new( .await { Ok(r) => r.count.unwrap_or(0), - Err(why) => { - error!("Getting user count: {}", why); + Err(err) => { + error!("Getting user count: {}", err); return Page::new( state .hbs @@ -118,8 +118,8 @@ pub async fn post_user_new( .await { Ok(r) => r.count.unwrap_or(0), - Err(why) => { - error!("Getting user count: {}", why); + Err(err) => { + error!("Getting user count: {}", err); return Page::new(state.hbs.render( "error.hbs", @@ -175,8 +175,8 @@ pub async fn post_user_new( let hashed_password = match bcrypt::hash(form_data.password, 12) { Ok(hash) => hash, - Err(why) => { - error!("Hashing password: {}", why); + Err(err) => { + error!("Hashing password: {}", err); return Page::new(state.hbs.render( "error.hbs", @@ -199,8 +199,8 @@ pub async fn post_user_new( .await { Ok(u) => u, - Err(why) => { - error!("Creating user: {}", why); + Err(err) => { + error!("Creating user: {}", err); return Page::new(state.hbs.render( "error.hbs", diff --git a/src/templates.rs b/src/templates.rs index 44dbb1c..d2bad5d 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -43,8 +43,8 @@ impl IntoResponse for Page { fn into_response(self) -> axum::response::Response { match self.0 { Ok(s) => ([(header::CONTENT_TYPE, "text/html; charset=utf-8")], s).into_response(), - Err(why) => { - error!("Error rendering page: {}", why); + Err(err) => { + error!("Error rendering page: {}", err); (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() } } From dd17a68cf221ae6c173618499fdc19b00799c3df Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 23 Oct 2023 17:34:06 +0200 Subject: [PATCH 2/2] working user signup! --- src/model/user.rs | 11 +++++ src/pages/user/mod.rs | 89 +++++++++++++++++++++++++++------- src/templates.rs | 20 ++++++-- src/token.rs | 8 ++- templates/new_user_confirm.hbs | 4 +- 5 files changed, 107 insertions(+), 25 deletions(-) diff --git a/src/model/user.rs b/src/model/user.rs index 2d01ab9..b4f4cba 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -49,6 +49,17 @@ impl From for Role { } } +impl Into for Role { + fn into(self) -> i32 { + match self { + Self::Viewer => 1, + Self::Editor => 2, + Self::Manager => 3, + Self::Admin => 4, + } + } +} + impl TryFrom<&str> for Role { type Error = eyre::Error; diff --git a/src/pages/user/mod.rs b/src/pages/user/mod.rs index ef1cc2f..56177f9 100644 --- a/src/pages/user/mod.rs +++ b/src/pages/user/mod.rs @@ -1,8 +1,9 @@ use std::{collections::BTreeMap, sync::Arc}; use axum::{ - body::BoxBody, + body::{BoxBody, Full}, extract::{Path, State}, + http::header::{CONTENT_TYPE, SET_COOKIE}, response::{IntoResponse, Response}, Form, }; @@ -11,10 +12,11 @@ use sqlx::{query, query_as}; use tracing::{error, info}; use crate::{ - extractor::ExtractUserToken, + extractor::{ExtractUserToken, TOKEN_COOKIE_NAME}, model::user::{Role, User}, state::AppState, templates::{Page, PageData}, + token::Claims, }; pub async fn get_user( @@ -59,19 +61,24 @@ pub async fn get_user_new( if user.role == Role::Admin { tracing::debug!("Authenticated user is admin"); - return Page::new( - state - .hbs - .render::("new_user.hbs", &Default::default()), - ); + return Page::new(state.hbs.render( + "new_user.hbs", + &PageData { + authed_user: Some(user), + ..Default::default() + }, + )); } tracing::debug!("Authenticated user is not an admin"); - return Page::new( - state - .hbs - .render::("error.hbs", &Default::default()), - ); + return Page::new(state.hbs.render::( + "error.hbs", + &PageData { + authed_user: Some(user), + message: Some("You are not an admin".into()), + ..Default::default() + }, + )); } tracing::debug!("There is no authenticated user"); @@ -189,11 +196,17 @@ pub async fn post_user_new( } }; + let role = match existing_user.is_some() { + true => Role::Viewer, + false => Role::Admin, + }; + let user = match query_as!( User, - r#"INSERT INTO users (username, password) VALUES ($1, $2) RETURNING *"#, + r#"INSERT INTO users (username, password, role) VALUES ($1, $2, $3) RETURNING *"#, form_data.username, hashed_password, + >::into(role), ) .fetch_one(&state.pool) .await @@ -226,11 +239,51 @@ pub async fn post_user_new( .into_response(); } - Page::new(state.hbs.render( - "new_user.hbs", + // Else, create cookie + let token = match Claims::new(&user).encode(&state.encoding_key) { + Ok(token) => token, + Err(err) => { + error!("Encoding token: {}", err); + + return Page::new(state.hbs.render("error.hbs", &PageData{ + message: Some("Internal server error. Note: your account was created successfully, please log in manually.".into()), + ..Default::default() + })).into_response(); + } + }; + + let content = match state.hbs.render( + "new_user_confirm.hbs", &PageData { - ..Default::default() + authed_user: Some(user.clone()), + user: Some(user), + message: Some("Your account has been created!".into()), }, - )) - .into_response() + ) { + Ok(content) => content, + Err(err) => { + error!("Rendering content: {}", err); + + return Page::new(state.hbs.render("error.hbs", &PageData{ + message: Some("Internal server error. Note: your account was created successfully, please log in manually.".into()), + ..Default::default() + })).into_response(); + } + }; + + return match Response::builder() + .header(SET_COOKIE, format!("{}={}", TOKEN_COOKIE_NAME, token)) + .header(CONTENT_TYPE, "text/html; charset=utf-8") + .body(Full::from(content)) + { + Ok(resp) => resp.into_response(), + Err(err) => { + error!("Building response: {}", err); + + return Page::new(state.hbs.render("error.hbs", &PageData{ + message: Some("Internal server error. Note: your account was created successfully, please log in manually.".into()), + ..Default::default() + })).into_response(); + } + }; } diff --git a/src/templates.rs b/src/templates.rs index d2bad5d..b3bc535 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,5 +1,6 @@ use axum::{ - http::{header, StatusCode}, + body::Full, + http::{header, Response, StatusCode}, response::IntoResponse, }; use eyre::{Context, Result}; @@ -42,10 +43,19 @@ impl Page { impl IntoResponse for Page { fn into_response(self) -> axum::response::Response { match self.0 { - Ok(s) => ([(header::CONTENT_TYPE, "text/html; charset=utf-8")], s).into_response(), + Ok(s) => Response::builder() + .header(header::CONTENT_TYPE, "text/html; charset=utf-8") + .body(Full::from(s)) + .unwrap() + .into_response(), Err(err) => { - error!("Error rendering page: {}", err); - (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() + error!("Rendering page: {}", err); + + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Full::from("Internal server error")) + .unwrap() + .into_response(); } } } @@ -57,4 +67,6 @@ pub struct PageData { // An optional flash message. Not all templates will handle this. pub message: Option, + + pub authed_user: Option, } diff --git a/src/token.rs b/src/token.rs index c78d045..b9df0d8 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,3 +1,4 @@ +use chrono::{Duration, Utc}; use jsonwebtoken::{errors::Error, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; @@ -12,8 +13,13 @@ pub struct Claims { impl Claims { pub fn new(user: &User) -> Self { + // Expire tokens after 90 days + // TODO: give tokens an ID to expire them earlier if necessary? Or a salt that's changed on + // password update + let now = Utc::now() + Duration::days(90); + Self { - exp: 0, + exp: now.timestamp() as usize, uid: user.id, role: user.role.into(), } diff --git a/templates/new_user_confirm.hbs b/templates/new_user_confirm.hbs index 33ee67b..ed6fd27 100644 --- a/templates/new_user_confirm.hbs +++ b/templates/new_user_confirm.hbs @@ -6,8 +6,8 @@ {{/if}}
    -
  • ID: {{new_user.id}}
  • -
  • Username: {{new_user.username}}
  • +
  • ID: {{user.id}}
  • +
  • Username: {{user.username}}
{{/inline}} {{> root.hbs}}