use axum::{ extract::{Path, State}, http::StatusCode, routing::{get, put}, Json, Router, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use uuid::Uuid; use crate::auth::AuthUser; use crate::models::user::UserResponse; pub fn router(pool: PgPool) -> Router { Router::new() .route("/api/users", get(list_users)) .route("/api/users/{username}", get(get_user_profile)) .route("/api/users/{username}/votes", get(get_user_votes)) .route("/api/users/me/profile", put(update_profile)) .with_state(pool) } async fn list_users(State(pool): State) -> Result>, String> { let users = sqlx::query_as!( crate::models::User, "SELECT * FROM users WHERE is_active = true ORDER BY created_at DESC LIMIT 100" ) .fetch_all(&pool) .await .map_err(|e| e.to_string())?; Ok(Json(users.into_iter().map(UserResponse::from).collect())) } #[derive(Debug, Serialize)] pub struct UserProfile { pub id: Uuid, pub username: String, pub display_name: Option, pub created_at: DateTime, pub communities: Vec, pub proposal_count: i64, pub comment_count: i64, } #[derive(Debug, Serialize)] pub struct CommunityMembership { pub id: Uuid, pub name: String, pub slug: String, pub role: String, } async fn get_user_profile( Path(username): Path, State(pool): State, ) -> Result, (StatusCode, String)> { let user = sqlx::query!( "SELECT id, username, display_name, created_at FROM users WHERE username = $1 AND is_active = true", username ) .fetch_optional(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? .ok_or((StatusCode::NOT_FOUND, "User not found".to_string()))?; let communities = sqlx::query!( r#" SELECT c.id, c.name, c.slug, cm.role FROM communities c JOIN community_members cm ON c.id = cm.community_id WHERE cm.user_id = $1 AND c.is_active = true ORDER BY cm.joined_at DESC "#, user.id ) .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; let proposal_count = sqlx::query_scalar!( "SELECT COUNT(*) FROM proposals WHERE author_id = $1", user.id ) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? .unwrap_or(0); let comment_count = sqlx::query_scalar!( "SELECT COUNT(*) FROM comments WHERE author_id = $1", user.id ) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? .unwrap_or(0); Ok(Json(UserProfile { id: user.id, username: user.username, display_name: user.display_name, created_at: user.created_at, communities: communities .into_iter() .map(|c| CommunityMembership { id: c.id, name: c.name, slug: c.slug, role: c.role, }) .collect(), proposal_count, comment_count, })) } #[derive(Debug, Deserialize)] pub struct UpdateProfile { pub display_name: Option, } async fn update_profile( auth: AuthUser, State(pool): State, Json(payload): Json, ) -> Result, (StatusCode, String)> { sqlx::query!( "UPDATE users SET display_name = $1 WHERE id = $2", payload.display_name, auth.user_id ) .execute(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(serde_json::json!({ "status": "updated" }))) } #[derive(Debug, Serialize)] pub struct UserVote { pub proposal_id: Uuid, pub proposal_title: String, pub community_name: String, pub option_label: String, pub voted_at: DateTime, } async fn get_user_votes( Path(username): Path, State(pool): State, ) -> Result>, (StatusCode, String)> { let user = sqlx::query!("SELECT id FROM users WHERE username = $1", username) .fetch_optional(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? .ok_or((StatusCode::NOT_FOUND, "User not found".to_string()))?; let votes = sqlx::query!( r#" SELECT v.created_at as voted_at, po.label as option_label, p.id as proposal_id, p.title as proposal_title, c.name as community_name FROM votes v JOIN voting_identities vi ON v.voter_id = vi.id JOIN proposal_options po ON v.option_id = po.id JOIN proposals p ON po.proposal_id = p.id JOIN communities c ON p.community_id = c.id WHERE vi.user_id = $1 ORDER BY v.created_at DESC LIMIT 20 "#, user.id ) .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; let result = votes .into_iter() .map(|v| UserVote { proposal_id: v.proposal_id, proposal_title: v.proposal_title, community_name: v.community_name, option_label: v.option_label, voted_at: v.voted_at, }) .collect(); Ok(Json(result)) }