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}}