1use axum::{
2 extract::{State, Path, Query},
3 http::{StatusCode, Response},
4 response::IntoResponse,
5 Json,
6};
7use sea_orm::{ColumnTrait, Condition, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder};
8use serde::{Deserialize, Serialize};
9use util::state::AppState;
10use validator::Validate;
11use crate::response::ApiResponse;
12use crate::routes::common::UserModule;
13use db::models::user::{Entity as UserEntity, Model as UserModel, Column as UserColumn};
14use std::{path::PathBuf};
15use tokio::fs::File;
16use tokio_util::io::ReaderStream;
17use axum::body::Body;
18use mime_guess::from_path;
19
20#[derive(Debug, Deserialize, Validate)]
21pub struct ListUsersQuery {
22 #[validate(range(min = 1))]
23 pub page: Option<u64>,
24 #[validate(range(min = 1, max = 100))]
25 pub per_page: Option<u64>,
26 pub sort: Option<String>,
27 pub query: Option<String>,
28 pub email: Option<String>,
29 pub username: Option<String>,
30 pub admin: Option<bool>,
31}
32
33#[derive(Debug, Serialize)]
34pub struct UserListItem {
35 pub id: String,
36 pub email: String,
37 pub username: String,
38 pub admin: bool,
39 pub created_at: String,
40 pub updated_at: String,
41}
42
43#[derive(Debug, Serialize)]
44pub struct UsersListResponse {
45 pub users: Vec<UserListItem>,
46 pub page: u64,
47 pub per_page: u64,
48 pub total: u64,
49}
50
51impl From<UserModel> for UserListItem {
52 fn from(user: UserModel) -> Self {
53 Self {
54 id: user.id.to_string(),
55 email: user.email,
56 username: user.username,
57 admin: user.admin,
58 created_at: user.created_at.to_string(),
59 updated_at: user.updated_at.to_string(),
60 }
61 }
62}
63
64pub async fn list_users(
119 State(app_state): State<AppState>,
120 Query(query): Query<ListUsersQuery>
121) -> impl IntoResponse {
122 let db = app_state.db();
123
124 if let Err(e) = query.validate() {
125 return (
126 StatusCode::BAD_REQUEST,
127 Json(ApiResponse::<UsersListResponse>::error(
128 common::format_validation_errors(&e),
129 )),
130 );
131 }
132
133 let page = query.page.unwrap_or(1);
134 let per_page = query.per_page.unwrap_or(20);
135
136 let mut condition = Condition::all();
137
138 if let Some(q) = &query.query {
139 let pattern = format!("%{}%", q.to_lowercase());
140 condition = condition.add(
141 Condition::any()
142 .add(UserColumn::Email.contains(&pattern))
143 .add(UserColumn::Username.contains(&pattern)),
144 );
145 }
146
147 if let Some(email) = &query.email {
148 condition = condition.add(UserColumn::Email.contains(&format!("%{}%", email)));
149 }
150
151 if let Some(sn) = &query.username {
152 condition = condition.add(UserColumn::Username.contains(&format!("%{}%", sn)));
153 }
154
155 if let Some(admin) = query.admin {
156 condition = condition.add(UserColumn::Admin.eq(admin));
157 }
158
159 let mut query_builder = UserEntity::find().filter(condition);
160
161 if let Some(sort_param) = &query.sort {
162 for sort_field in sort_param.split(',') {
163 let (field, desc) = if sort_field.starts_with('-') {
164 (&sort_field[1..], true)
165 } else {
166 (sort_field, false)
167 };
168
169 match field {
170 "email" => {
171 query_builder = if desc {
172 query_builder.order_by_desc(UserColumn::Email)
173 } else {
174 query_builder.order_by_asc(UserColumn::Email)
175 };
176 }
177 "username" => {
178 query_builder = if desc {
179 query_builder.order_by_desc(UserColumn::Username)
180 } else {
181 query_builder.order_by_asc(UserColumn::Username)
182 };
183 }
184 "created_at" => {
185 query_builder = if desc {
186 query_builder.order_by_desc(UserColumn::CreatedAt)
187 } else {
188 query_builder.order_by_asc(UserColumn::CreatedAt)
189 };
190 }
191 "admin" => {
192 query_builder = if desc {
193 query_builder.order_by_desc(UserColumn::Admin)
194 } else {
195 query_builder.order_by_asc(UserColumn::Admin)
196 };
197 }
198 _ => {}
199 }
200 }
201 } else {
202 query_builder = query_builder.order_by_asc(UserColumn::Id);
203 }
204
205 let paginator = query_builder.paginate(db, per_page);
206 let total = paginator.num_items().await.unwrap_or(0);
207 let users = paginator.fetch_page(page - 1).await.unwrap_or_default();
208 let users = users.into_iter().map(UserListItem::from).collect();
209
210 (
211 StatusCode::OK,
212 Json(ApiResponse::success(
213 UsersListResponse {
214 users,
215 page,
216 per_page,
217 total,
218 },
219 "Users retrieved successfully",
220 )),
221 )
222}
223
224pub async fn get_user(
237 State(app_state): State<AppState>,
238 Path(user_id): Path<i64>
239) -> impl IntoResponse {
240 let db = app_state.db();
241
242 match UserEntity::find_by_id(user_id).one(db).await {
243 Ok(Some(user)) => {
244 let user_item = UserListItem::from(user);
245 (
246 StatusCode::OK,
247 Json(ApiResponse::success(user_item, "User retrieved successfully")),
248 )
249 }
250 Ok(None) => (
251 StatusCode::NOT_FOUND,
252 Json(ApiResponse::<UserListItem>::error("User not found")),
253 ),
254 Err(err) => (
255 StatusCode::INTERNAL_SERVER_ERROR,
256 Json(ApiResponse::<UserListItem>::error(format!("Database error: {}", err))),
257 ),
258 }
259}
260
261pub async fn get_user_modules(
316 State(app_state): State<AppState>,
317 Path(user_id): Path<i64>
318) -> impl IntoResponse {
319 let db = app_state.db();
320
321 let roles = match UserModel::get_module_roles(db, user_id).await {
322 Ok(r) => r,
323 Err(e) => {
324 return (
325 StatusCode::INTERNAL_SERVER_ERROR,
326 Json(ApiResponse::<Vec<UserModule>>::error(format!("Database error: {}", e))),
327 );
328 }
329 };
330
331 let modules = roles
332 .into_iter()
333 .map(|r| UserModule {
334 id: r.module_id,
335 code: r.module_code,
336 year: r.module_year,
337 description: r.module_description.unwrap_or_default(),
338 credits: r.module_credits,
339 role: r.role,
340 created_at: r.module_created_at,
341 updated_at: r.module_updated_at,
342 })
343 .collect::<Vec<_>>();
344
345 (
346 StatusCode::OK,
347 Json(ApiResponse::success(
348 modules,
349 "Modules for user retrieved successfully",
350 )),
351 )
352}
353
354pub async fn get_avatar(
358 State(_state): State<AppState>,
359 Path(user_id): Path<i64>,
360) -> impl IntoResponse {
361 let root = std::env::var("USER_PROFILE_STORAGE_ROOT")
362 .unwrap_or_else(|_| "data/user_profile_pictures".to_string());
363
364 let avatar_path = PathBuf::from(&root).join(format!("user_{}/avatar", user_id));
365
366 for ext in ["jpg", "png", "gif"] {
368 let try_path = avatar_path.with_extension(ext);
369 if try_path.exists() {
370 let file = match File::open(&try_path).await {
371 Ok(f) => f,
372 Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
373 };
374
375 let mime = from_path(&try_path).first_or_octet_stream();
376 let stream = ReaderStream::new(file);
377 let body = Body::from_stream(stream);
378
379 return Response::builder()
380 .header("Content-Type", mime.as_ref())
381 .body(body)
382 .unwrap();
383 }
384 }
385
386 StatusCode::NOT_FOUND.into_response()
387}