Edit profile and templates update
All checks were successful
Rust / Test (push) Successful in 6m6s
Rust / Rustfmt (push) Successful in 22s
Rust / Clippy (push) Successful in 1m36s
Rust / Code coverage (push) Successful in 4m47s

This commit is contained in:
Alphonse Paix
2025-10-06 19:13:51 +02:00
parent da590fb7c6
commit b252216709
27 changed files with 596 additions and 262 deletions

View File

@@ -161,6 +161,7 @@ impl From<AuthError> for AppError {
}
pub async fn not_found() -> Response {
tracing::error!("Not found.");
not_found_html()
}

View File

@@ -12,6 +12,7 @@ use crate::{
templates::{HtmlTemplate, MessageTemplate},
};
use anyhow::Context;
use axum::response::Redirect;
use axum::{
extract::Request,
middleware::Next,
@@ -49,11 +50,17 @@ pub async fn require_auth(
mut request: Request,
next: Next,
) -> Result<Response, AppError> {
let user_id = session
let user_id = match session
.get_user_id()
.await
.map_err(|e| AdminError::UnexpectedError(e.into()))?
.ok_or(AdminError::NotAuthenticated)?;
{
None => {
tracing::error!("Not authenticated. Redirecting to /login.");
return Ok(Redirect::to("/login").into_response());
}
Some(user_id) => user_id,
};
let username = session
.get_username()
.await

View File

@@ -29,7 +29,7 @@ pub async fn get_login(session: TypedSession) -> Result<Response, AppError> {
.context("Failed to retrieve user id from data store.")?
.is_some()
{
Ok(Redirect::to("/admin/dashboard").into_response())
Ok(Redirect::to("dashboard").into_response())
} else {
Ok(Html(LoginTemplate.render().unwrap()).into_response())
}
@@ -66,6 +66,6 @@ pub async fn post_login(
.context("Failed to insert role in session data store.")?;
let mut headers = HeaderMap::new();
headers.insert("HX-Redirect", "/admin/dashboard".parse().unwrap());
headers.insert("HX-Redirect", "/dashboard".parse().unwrap());
Ok((StatusCode::OK, headers).into_response())
}

View File

@@ -1,5 +1,7 @@
use crate::authentication::AuthenticatedUser;
use crate::routes::verify_password;
use crate::templates::MessageTemplate;
use crate::session_state::TypedSession;
use crate::templates::{ErrorTemplate, MessageTemplate, UserEditTemplate};
use crate::{
authentication::Role,
domain::{PostEntry, UserEntry},
@@ -9,7 +11,7 @@ use crate::{
};
use anyhow::Context;
use axum::{
Form,
Extension, Form,
extract::{Path, State},
response::{IntoResponse, Response},
};
@@ -17,9 +19,102 @@ use secrecy::{ExposeSecret, SecretString};
use sqlx::PgPool;
use uuid::Uuid;
pub async fn get_user_edit(
Path(username): Path<String>,
Extension(AuthenticatedUser {
user_id,
username: session_username,
..
}): Extension<AuthenticatedUser>,
State(AppState {
connection_pool, ..
}): State<AppState>,
) -> Result<Response, AppError> {
if username != session_username {
let template = HtmlTemplate(ErrorTemplate::Forbidden);
return Ok(template.into_response());
}
let user = sqlx::query_as!(
UserEntry,
r#"
SELECT user_id, username, role as "role: Role", full_name, bio, member_since
FROM users
WHERE user_id = $1
"#,
user_id
)
.fetch_one(&connection_pool)
.await
.context("Could not fetch user in database.")?;
let template = HtmlTemplate(UserEditTemplate { user });
Ok(template.into_response())
}
#[derive(serde::Deserialize)]
pub struct ProfilePath {
pub struct EditProfileForm {
username: String,
full_name: String,
bio: String,
}
pub async fn put_user_edit(
State(AppState {
connection_pool, ..
}): State<AppState>,
session: TypedSession,
Extension(AuthenticatedUser {
user_id,
username: session_username,
..
}): Extension<AuthenticatedUser>,
Path(username): Path<String>,
Form(form): Form<EditProfileForm>,
) -> Result<Response, AppError> {
if username != session_username {
let template = HtmlTemplate(ErrorTemplate::Forbidden);
return Ok(template.into_response());
}
let updated_username = form.username.trim();
if updated_username != session_username
&& sqlx::query!(
"SELECT user_id FROM users WHERE username = $1",
updated_username
)
.fetch_optional(&connection_pool)
.await
.context("Could not fetch users table.")?
.is_some()
{
let template = HtmlTemplate(MessageTemplate::error(
"The username is already taken.".into(),
));
return Ok(template.into_response());
}
let updated_full_name = form.full_name.trim();
let bio = form.bio.trim();
sqlx::query!(
"
UPDATE users
SET username = $1, full_name = $2, bio = $3
WHERE user_id = $4
",
updated_username,
updated_full_name,
bio,
user_id
)
.execute(&connection_pool)
.await
.context("Failed to apply changes.")
.map_err(AppError::FormError)?;
session
.insert_username(updated_username.to_owned())
.await
.context("Could not update session username.")?;
let template = HtmlTemplate(MessageTemplate::success(
"Your profile has been updated.".into(),
));
Ok(template.into_response())
}
#[tracing::instrument(name = "Get users from database", skip(connection_pool))]
@@ -144,12 +239,13 @@ pub async fn delete_user(
Ok(template.into_response())
}
#[tracing::instrument(name = "Fetching user data", skip(connection_pool))]
#[tracing::instrument(name = "Fetching user data", skip(connection_pool, session))]
pub async fn user_profile(
session: TypedSession,
State(AppState {
connection_pool, ..
}): State<AppState>,
Path(ProfilePath { username }): Path<ProfilePath>,
Path(username): Path<String>,
) -> Result<Response, AppError> {
match fetch_user_data(&connection_pool, &username)
.await
@@ -159,7 +255,15 @@ pub async fn user_profile(
let posts = fetch_user_posts(&connection_pool, &user.user_id)
.await
.context("Could not fetch user posts.")?;
let template = HtmlTemplate(UserTemplate { user, posts });
let session_username = session
.get_username()
.await
.context("Could not fetch session username.")?;
let template = HtmlTemplate(UserTemplate {
user,
session_username,
posts,
});
Ok(template.into_response())
}
None => {

View File

@@ -100,7 +100,11 @@ pub fn app(
.route("/newsletters", post(publish_newsletter))
.route("/posts", post(create_post))
.route("/logout", get(logout))
.merge(admin_routes)
.route(
"/users/{username}/edit",
get(get_user_edit).put(put_user_edit),
)
.nest("/admin", admin_routes)
.layer(middleware::from_fn(require_auth));
Router::new()
.route("/", get(home))
@@ -119,7 +123,7 @@ pub fn app(
)
.route("/users/{username}", get(user_profile))
.route("/favicon.ico", get(favicon))
.nest("/admin", auth_routes)
.merge(auth_routes)
.nest_service("/assets", ServeDir::new("assets"))
.layer(
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {

View File

@@ -25,9 +25,16 @@ where
#[template(path = "user/profile.html")]
pub struct UserTemplate {
pub user: UserEntry,
pub session_username: Option<String>,
pub posts: Vec<PostEntry>,
}
#[derive(Template)]
#[template(path = "user/edit.html")]
pub struct UserEditTemplate {
pub user: UserEntry,
}
#[derive(Template)]
#[template(path = "message.html")]
pub struct MessageTemplate {
@@ -147,6 +154,8 @@ pub enum ErrorTemplate {
NotFound,
#[template(path = "error/500.html")]
InternalServer,
#[template(path = "error/403.html")]
Forbidden,
}
#[derive(Template)]