fullstack: added profile pictures/avatars to accounts and improved settings page layout

This commit is contained in:
2026-01-11 18:04:24 +01:00
parent 52a6231d0b
commit b512f9d65f
3 changed files with 104 additions and 14 deletions

View File

@@ -1,17 +1,18 @@
use axum::{
Extension, Json, Router,
extract::Request,
extract::{Path, Request},
http::{HeaderMap, StatusCode},
middleware::Next,
response::Response,
routing::{get, post, put},
};
use sqlx::PgPool;
use std::env;
use std::{env, sync::Arc};
use uuid::Uuid;
use validator::ValidateEmail;
use crate::{
AppConfig,
auth::{create_jwt, hash_password, validate_token, verify_jwt, verify_password},
db::{user_id_from_uuid, username_from_uuid},
};
@@ -65,7 +66,9 @@ pub fn routes() -> Router {
.route("/login", post(login))
.route("/register", post(register_user))
.route("/validate-token", get(validate_token))
.route("/account", put(update_user))
.route("/account/settings", put(update_user))
.route("/account/upload-avatar", post(upload_avatar))
.route("/account/get-avatar/{uuid}", get(get_avatar))
.layer(axum::middleware::from_fn(registration_guard))
}
@@ -264,3 +267,75 @@ pub async fn update_user(
}),
))
}
async fn upload_avatar(
headers: HeaderMap,
Extension(db): Extension<PgPool>,
Extension(config): Extension<Arc<AppConfig>>,
body: axum::body::Bytes,
) -> Result<StatusCode, (StatusCode, String)> {
let claims = verify_jwt(headers)?;
let user_id = user_id_from_uuid(&db, claims.sub).await?;
tracing::info!("User ID {} is uploading {} bytes)", user_id, body.len());
let base_dir = &config.avatar_dir;
let file_extension = "png"; // TODO: detect MIME type
let filename = format!("{}.{}", claims.sub, file_extension);
let full_path = std::path::Path::new(&base_dir).join(&filename);
tokio::fs::create_dir_all(&base_dir).await.map_err(|e| {
tracing::error!("Failed to create storage: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to upload file".into(),
)
})?;
tokio::fs::write(&full_path, body).await.map_err(|e| {
tracing::error!("Failed to save file: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to upload file".into(),
)
})?;
sqlx::query("UPDATE user_ SET avatar_url = $1 WHERE id = $2")
.bind(filename)
.bind(user_id)
.execute(&db)
.await
.map_err(|e| {
tracing::error!("DB error: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "DB erorr".into())
})?;
Ok(StatusCode::OK)
}
// Public route
async fn get_avatar(
Path(uuid): Path<Uuid>,
Extension(config): Extension<Arc<AppConfig>>,
) -> Result<Response, (StatusCode, String)> {
let base_dir = &config.avatar_dir;
let filename = format!("{}.png", uuid);
let full_path = std::path::Path::new(&base_dir).join(filename);
if !full_path.exists() {
return Err((StatusCode::NOT_FOUND, "Avatar not found".into()));
}
let file_contents = tokio::fs::read(&full_path).await.map_err(|e| {
tracing::error!("Could not read avatar file: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
"Could not read file".into(),
)
})?;
Ok(Response::builder()
.header("Content-Type", "image/png")
.body(axum::body::Body::from(file_contents))
.unwrap())
}