likwid/backend/src/api/users.rs

189 lines
5.3 KiB
Rust
Raw Normal View History

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<PgPool>,
) -> Result<Json<Vec<UserResponse>>, 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<String>,
pub created_at: DateTime<Utc>,
pub communities: Vec<CommunityMembership>,
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<String>,
State(pool): State<PgPool>,
) -> Result<Json<UserProfile>, (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<String>,
}
async fn update_profile(
auth: AuthUser,
State(pool): State<PgPool>,
Json(payload): Json<UpdateProfile>,
) -> Result<Json<serde_json::Value>, (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<Utc>,
}
async fn get_user_votes(
Path(username): Path<String>,
State(pool): State<PgPool>,
) -> Result<Json<Vec<UserVote>>, (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))
}