Edit posts
All checks were successful
Rust / Test (push) Successful in 5m43s
Rust / Rustfmt (push) Successful in 22s
Rust / Clippy (push) Successful in 1m37s
Rust / Code coverage (push) Successful in 4m49s

Use fix routes for user profile edit handles to make it easier when user decides to change his username
This commit is contained in:
Alphonse Paix
2025-10-06 22:33:05 +02:00
parent b252216709
commit 8b5f55db6f
12 changed files with 313 additions and 180 deletions

View File

@@ -1,10 +1,11 @@
use crate::routes::{COMMENTS_PER_PAGE, get_max_page};
use crate::templates::PostsPageDashboardTemplate;
use crate::authentication::AuthenticatedUser;
use crate::routes::{COMMENTS_PER_PAGE, Query, get_max_page};
use crate::session_state::TypedSession;
use crate::templates::{ErrorTemplate, MessageTemplate, PostsPageDashboardTemplate};
use crate::{
domain::PostEntry,
routes::{
AppError, Path, Query, get_comments_count_for_post, get_comments_page_for_post,
not_found_html,
AppError, Path, get_comments_count_for_post, get_comments_page_for_post, not_found_html,
},
startup::AppState,
templates::{HtmlTemplate, PostListTemplate, PostTemplate, PostsTemplate},
@@ -12,6 +13,7 @@ use crate::{
use anyhow::Context;
use askama::Template;
use axum::{
Extension, Form,
extract::State,
response::{Html, IntoResponse, Redirect, Response},
};
@@ -118,17 +120,65 @@ pub async fn get_posts_count(connection_pool: &PgPool) -> Result<i64, sqlx::Erro
}
#[derive(serde::Deserialize)]
pub struct PostParams {
pub struct EditPostForm {
pub title: String,
pub content: String,
}
#[tracing::instrument(name = "Editing post", skip_all, fields(post_id = %post_id))]
pub async fn update_post(
State(AppState {
connection_pool, ..
}): State<AppState>,
Extension(AuthenticatedUser { user_id, .. }): Extension<AuthenticatedUser>,
Path(post_id): Path<Uuid>,
Form(form): Form<EditPostForm>,
) -> Result<Response, AppError> {
let record = sqlx::query!("SELECT author_id FROM posts WHERE post_id = $1", post_id)
.fetch_optional(&connection_pool)
.await
.context("Could not fetch post author.")?;
match record {
None => Ok(HtmlTemplate(ErrorTemplate::NotFound).into_response()),
Some(record) if record.author_id == user_id => {
sqlx::query!(
"
UPDATE posts
SET title = $1, content = $2 WHERE post_id = $3
",
form.title,
form.content,
post_id
)
.execute(&connection_pool)
.await
.context("Could not update post")?;
Ok(HtmlTemplate(MessageTemplate::success(
"Your changes have been saved.".into(),
))
.into_response())
}
_ => Ok(HtmlTemplate(ErrorTemplate::Forbidden).into_response()),
}
}
#[derive(serde::Deserialize)]
pub struct OriginQueryParam {
origin: Option<Uuid>,
}
#[tracing::instrument(name = "Fetching post from database", skip(connection_pool, origin))]
#[tracing::instrument(
name = "Fetching post from database",
skip(connection_pool, origin, session)
)]
pub async fn see_post(
session: TypedSession,
State(AppState {
connection_pool, ..
}): State<AppState>,
Path(post_id): Path<Uuid>,
Query(PostParams { origin }): Query<PostParams>,
Query(OriginQueryParam { origin }): Query<OriginQueryParam>,
) -> Result<Response, AppError> {
if let Some(origin) = origin {
mark_email_as_opened(&connection_pool, origin).await?;
@@ -140,7 +190,7 @@ pub async fn see_post(
.context(format!("Failed to fetch post #{}", post_id))
.map_err(AppError::unexpected_page)?
{
let post = post
let post_html = post
.to_html()
.context("Could not render markdown with extension.")?;
let current_page = 1;
@@ -152,13 +202,19 @@ pub async fn see_post(
.await
.context("Failed to fetch latest comments")?;
let idempotency_key = Uuid::new_v4().to_string();
let session_username = session
.get_username()
.await
.context("Could not check for session username")?;
let template = HtmlTemplate(PostTemplate {
post,
post_html,
comments,
idempotency_key,
current_page,
max_page,
comments_count,
session_username,
});
Ok(template.into_response())
} else {

View File

@@ -19,21 +19,12 @@ 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>,
pub async fn user_edit_form(
Extension(AuthenticatedUser { user_id, .. }): 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#"
@@ -52,25 +43,26 @@ pub async fn get_user_edit(
#[derive(serde::Deserialize)]
pub struct EditProfileForm {
user_id: Uuid,
username: String,
full_name: String,
bio: String,
}
pub async fn put_user_edit(
#[tracing::instrument(name = "Updating user profile", skip_all, fields(user_id = %form.user_id))]
pub async fn update_user(
State(AppState {
connection_pool, ..
}): State<AppState>,
session: TypedSession,
Extension(AuthenticatedUser {
user_id,
user_id: session_user_id,
username: session_username,
..
}): Extension<AuthenticatedUser>,
Path(username): Path<String>,
Form(form): Form<EditProfileForm>,
) -> Result<Response, AppError> {
if username != session_username {
if form.user_id != session_user_id {
let template = HtmlTemplate(ErrorTemplate::Forbidden);
return Ok(template.into_response());
}
@@ -101,7 +93,7 @@ pub async fn put_user_edit(
updated_username,
updated_full_name,
bio,
user_id
form.user_id
)
.execute(&connection_pool)
.await