Compare commits

...

2 commits

Author SHA1 Message Date
sam
dd17a68cf2
working user signup! 2023-10-23 17:34:06 +02:00
sam
c5c884f069
consistently name Err values err 2023-10-23 01:53:05 +02:00
6 changed files with 123 additions and 39 deletions

View file

@ -11,6 +11,8 @@ use crate::{model::user::User, state::AppState, token::Claims};
pub struct ExtractUserToken(pub Option<User>); pub struct ExtractUserToken(pub Option<User>);
pub const TOKEN_COOKIE_NAME: &'static str = "imgboard-token";
#[async_trait] #[async_trait]
impl<S> FromRequestParts<S> for ExtractUserToken impl<S> FromRequestParts<S> for ExtractUserToken
where where
@ -22,8 +24,8 @@ where
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let state = match parts.extract_with_state::<AppState, _>(state).await { let state = match parts.extract_with_state::<AppState, _>(state).await {
Ok(s) => s, Ok(s) => s,
Err(why) => { Err(err) => {
error!("Getting state: {}", why); error!("Getting state: {}", err);
return Err((StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")); 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) => { Some(token) => {
let claims = Claims::decode(token, &state.decoding_key).map_err(|e| { let claims = Claims::decode(token, &state.decoding_key).map_err(|e| {
error!("Decoding token claims: {}", e); error!("Decoding token claims: {}", e);

View file

@ -49,6 +49,17 @@ impl From<i32> for Role {
} }
} }
impl Into<i32> 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 { impl TryFrom<&str> for Role {
type Error = eyre::Error; type Error = eyre::Error;

View file

@ -1,8 +1,9 @@
use std::{collections::BTreeMap, sync::Arc}; use std::{collections::BTreeMap, sync::Arc};
use axum::{ use axum::{
body::BoxBody, body::{BoxBody, Full},
extract::{Path, State}, extract::{Path, State},
http::header::{CONTENT_TYPE, SET_COOKIE},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Form, Form,
}; };
@ -11,10 +12,11 @@ use sqlx::{query, query_as};
use tracing::{error, info}; use tracing::{error, info};
use crate::{ use crate::{
extractor::ExtractUserToken, extractor::{ExtractUserToken, TOKEN_COOKIE_NAME},
model::user::{Role, User}, model::user::{Role, User},
state::AppState, state::AppState,
templates::{Page, PageData}, templates::{Page, PageData},
token::Claims,
}; };
pub async fn get_user( pub async fn get_user(
@ -26,8 +28,8 @@ pub async fn get_user(
.await .await
{ {
Ok(user) => user, Ok(user) => user,
Err(why) => { Err(err) => {
error!("Getting user: {}", why); error!("Getting user: {}", err);
return Page::new(state.hbs.render( return Page::new(state.hbs.render(
"error.hbs", "error.hbs",
&PageData { &PageData {
@ -59,19 +61,24 @@ pub async fn get_user_new(
if user.role == Role::Admin { if user.role == Role::Admin {
tracing::debug!("Authenticated user is admin"); tracing::debug!("Authenticated user is admin");
return Page::new( return Page::new(state.hbs.render(
state "new_user.hbs",
.hbs &PageData {
.render::<PageData>("new_user.hbs", &Default::default()), authed_user: Some(user),
); ..Default::default()
},
));
} }
tracing::debug!("Authenticated user is not an admin"); tracing::debug!("Authenticated user is not an admin");
return Page::new( return Page::new(state.hbs.render::<PageData>(
state "error.hbs",
.hbs &PageData {
.render::<PageData>("error.hbs", &Default::default()), authed_user: Some(user),
); message: Some("You are not an admin".into()),
..Default::default()
},
));
} }
tracing::debug!("There is no authenticated user"); tracing::debug!("There is no authenticated user");
@ -82,8 +89,8 @@ pub async fn get_user_new(
.await .await
{ {
Ok(r) => r.count.unwrap_or(0), Ok(r) => r.count.unwrap_or(0),
Err(why) => { Err(err) => {
error!("Getting user count: {}", why); error!("Getting user count: {}", err);
return Page::new( return Page::new(
state state
.hbs .hbs
@ -118,8 +125,8 @@ pub async fn post_user_new(
.await .await
{ {
Ok(r) => r.count.unwrap_or(0), Ok(r) => r.count.unwrap_or(0),
Err(why) => { Err(err) => {
error!("Getting user count: {}", why); error!("Getting user count: {}", err);
return Page::new(state.hbs.render( return Page::new(state.hbs.render(
"error.hbs", "error.hbs",
@ -175,8 +182,8 @@ pub async fn post_user_new(
let hashed_password = match bcrypt::hash(form_data.password, 12) { let hashed_password = match bcrypt::hash(form_data.password, 12) {
Ok(hash) => hash, Ok(hash) => hash,
Err(why) => { Err(err) => {
error!("Hashing password: {}", why); error!("Hashing password: {}", err);
return Page::new(state.hbs.render( return Page::new(state.hbs.render(
"error.hbs", "error.hbs",
@ -189,18 +196,24 @@ pub async fn post_user_new(
} }
}; };
let role = match existing_user.is_some() {
true => Role::Viewer,
false => Role::Admin,
};
let user = match query_as!( let user = match query_as!(
User, 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, form_data.username,
hashed_password, hashed_password,
<Role as Into<i32>>::into(role),
) )
.fetch_one(&state.pool) .fetch_one(&state.pool)
.await .await
{ {
Ok(u) => u, Ok(u) => u,
Err(why) => { Err(err) => {
error!("Creating user: {}", why); error!("Creating user: {}", err);
return Page::new(state.hbs.render( return Page::new(state.hbs.render(
"error.hbs", "error.hbs",
@ -226,11 +239,51 @@ pub async fn post_user_new(
.into_response(); .into_response();
} }
Page::new(state.hbs.render( // Else, create cookie
"new_user.hbs", let token = match Claims::new(&user).encode(&state.encoding_key) {
&PageData { 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() ..Default::default()
}, })).into_response();
)) }
.into_response() };
let content = match state.hbs.render(
"new_user_confirm.hbs",
&PageData {
authed_user: Some(user.clone()),
user: Some(user),
message: Some("Your account has been created!".into()),
},
) {
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();
}
};
} }

View file

@ -1,5 +1,6 @@
use axum::{ use axum::{
http::{header, StatusCode}, body::Full,
http::{header, Response, StatusCode},
response::IntoResponse, response::IntoResponse,
}; };
use eyre::{Context, Result}; use eyre::{Context, Result};
@ -42,10 +43,19 @@ impl Page {
impl IntoResponse for Page { impl IntoResponse for Page {
fn into_response(self) -> axum::response::Response { fn into_response(self) -> axum::response::Response {
match self.0 { match self.0 {
Ok(s) => ([(header::CONTENT_TYPE, "text/html; charset=utf-8")], s).into_response(), Ok(s) => Response::builder()
Err(why) => { .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
error!("Error rendering page: {}", why); .body(Full::from(s))
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response() .unwrap()
.into_response(),
Err(err) => {
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. // An optional flash message. Not all templates will handle this.
pub message: Option<String>, pub message: Option<String>,
pub authed_user: Option<User>,
} }

View file

@ -1,3 +1,4 @@
use chrono::{Duration, Utc};
use jsonwebtoken::{errors::Error, DecodingKey, EncodingKey, Header, Validation}; use jsonwebtoken::{errors::Error, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -12,8 +13,13 @@ pub struct Claims {
impl Claims { impl Claims {
pub fn new(user: &User) -> Self { 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 { Self {
exp: 0, exp: now.timestamp() as usize,
uid: user.id, uid: user.id,
role: user.role.into(), role: user.role.into(),
} }

View file

@ -6,8 +6,8 @@
</div> </div>
{{/if}} {{/if}}
<ul> <ul>
<li>ID: {{new_user.id}}</li> <li>ID: {{user.id}}</li>
<li>Username: {{new_user.username}}</li> <li>Username: {{user.username}}</li>
</ul> </ul>
{{/inline}} {{/inline}}
{{> root.hbs}} {{> root.hbs}}