1use std::path::PathBuf;
2use axum::{
3 extract::{State, Path},
4 http::StatusCode,
5 response::IntoResponse,
6 Json,
7};
8use axum::extract::Multipart;
9use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, EntityTrait, IntoActiveModel, QueryFilter, Set};
10use serde::Deserialize;
11use tokio::fs;
12use tokio::io::AsyncWriteExt;
13use util::state::AppState;
14use validator::Validate;
15use crate::{response::ApiResponse};
16use common::format_validation_errors;
17use db::models::user;
18use crate::routes::common::UserResponse;
19
20#[derive(Debug, Deserialize, Validate)]
21pub struct UpdateUserRequest {
22 pub username: Option<String>,
23
24 #[validate(email(message = "Invalid email format"))]
25 pub email: Option<String>,
26
27 pub admin: Option<bool>,
28}
29
30lazy_static::lazy_static! {
31 static ref username_REGEX: regex::Regex = regex::Regex::new("^u\\d{8}$").unwrap();
32}
33
34pub async fn update_user(
100 State(app_state): State<AppState>,
101 Path(user_id): Path<i64>,
102 Json(req): Json<UpdateUserRequest>,
103) -> impl IntoResponse {
104 let db = app_state.db();
105
106 if let Err(e) = req.validate() {
107 return (
108 StatusCode::BAD_REQUEST,
109 Json(ApiResponse::<UserResponse>::error(format_validation_errors(&e))),
110 );
111 }
112
113 if req.username.is_none() && req.email.is_none() && req.admin.is_none() {
114 return (
115 StatusCode::BAD_REQUEST,
116 Json(ApiResponse::<UserResponse>::error("At least one field must be provided")),
117 );
118 }
119
120 let current_user = user::Entity::find_by_id(user_id)
121 .one(db).await.unwrap().unwrap();
122
123 if let Some(_) = req.admin {
126 return (
127 StatusCode::FORBIDDEN,
128 Json(ApiResponse::<UserResponse>::error("Changing admin status is not allowed")),
129 );
130 }
131
132 if let Some(email) = &req.email {
133 if email != ¤t_user.email {
134 let exists_result = user::Entity::find()
135 .filter(
136 Condition::all()
137 .add(user::Column::Email.eq(email.clone()))
138 .add(user::Column::Id.ne(user_id)),
139 )
140 .one(db)
141 .await;
142
143 match exists_result {
144 Ok(Some(_)) => {
145 return (
146 StatusCode::CONFLICT,
147 Json(ApiResponse::<UserResponse>::error("A user with this email already exists")),
148 );
149 }
150 Ok(None) => {}
151 Err(e) => {
152 return (
153 StatusCode::INTERNAL_SERVER_ERROR,
154 Json(ApiResponse::<UserResponse>::error(format!("Database error: {}", e))),
155 );
156 }
157 }
158 }
159 }
160
161 if let Some(sn) = &req.username {
162 if sn != ¤t_user.username {
163 let exists_result = user::Entity::find()
164 .filter(
165 Condition::all()
166 .add(user::Column::Username.eq(sn.clone()))
167 .add(user::Column::Id.ne(user_id)),
168 )
169 .one(db)
170 .await;
171
172 match exists_result {
173 Ok(Some(_)) => {
174 return (
175 StatusCode::CONFLICT,
176 Json(ApiResponse::<UserResponse>::error(
177 "A user with this student number already exists",
178 )),
179 );
180 }
181 Ok(None) => {}
182 Err(e) => {
183 return (
184 StatusCode::INTERNAL_SERVER_ERROR,
185 Json(ApiResponse::<UserResponse>::error(format!("Database error: {}", e))),
186 );
187 }
188 }
189 }
190 }
191
192 let mut active_model: user::ActiveModel = current_user.into();
193 if let Some(sn) = req.username {
194 active_model.username = Set(sn);
195 }
196 if let Some(email) = req.email {
197 active_model.email = Set(email);
198 }
199 if let Some(admin) = req.admin {
200 active_model.admin = Set(admin);
201 }
202
203 match active_model.update(db).await {
204 Ok(updated) => (
205 StatusCode::OK,
206 Json(ApiResponse::success(
207 UserResponse::from(updated),
208 "User updated successfully",
209 )),
210 ),
211 Err(e) => (
212 StatusCode::INTERNAL_SERVER_ERROR,
213 Json(ApiResponse::<UserResponse>::error(format!("Database error: {}", e))),
214 ),
215 }
216}
217
218#[derive(serde::Serialize)]
219struct ProfilePictureResponse {
220 profile_picture_path: String,
221}
222
223pub async fn upload_avatar(
278 State(app_state): State<AppState>,
279 Path(user_id): Path<i64>,
280 mut multipart: Multipart,
281) -> impl IntoResponse {
282 let db = app_state.db();
283
284 const MAX_SIZE: u64 = 2 * 1024 * 1024;
285 const ALLOWED_MIME: &[&str] = &["image/jpeg", "image/png", "image/gif"];
286
287 let mut content_type = None;
288 let mut file_data = None;
289
290 while let Some(field) = multipart.next_field().await.unwrap_or(None) {
291 if field.name() == Some("file") {
292 content_type = field.content_type().map(|ct| ct.to_string());
293
294 if let Some(ct) = &content_type {
295 if !ALLOWED_MIME.contains(&ct.as_str()) {
296 return (
297 StatusCode::BAD_REQUEST,
298 Json(ApiResponse::<ProfilePictureResponse>::error("File type not supported.")),
299 )
300 }
301 }
302
303 let bytes = field.bytes().await.unwrap();
304 if bytes.len() as u64 > MAX_SIZE {
305 return (
306 StatusCode::BAD_REQUEST,
307 Json(ApiResponse::<ProfilePictureResponse>::error("File too large.")),
308 )
309 }
310
311 file_data = Some(bytes);
312 }
313 }
314
315 let Some(file_bytes) = file_data else {
316 return (
317 StatusCode::BAD_REQUEST,
318 Json(ApiResponse::<ProfilePictureResponse>::error("No file uploaded.")),
319 )
320 };
321
322 let ext = match content_type.as_deref() {
323 Some("image/png") => "png",
324 Some("image/jpeg") => "jpg",
325 Some("image/gif") => "gif",
326 _ => "bin",
327 };
328
329 let root = std::env::var("USER_PROFILE_STORAGE_ROOT")
330 .unwrap_or_else(|_| "data/user_profile_pictures".to_string());
331
332 let user_dir = PathBuf::from(&root).join(format!("user_{}", user_id));
333 let _ = fs::create_dir_all(&user_dir);
334
335 let filename = format!("avatar.{}", ext);
336 let path = user_dir.join(&filename);
337 let mut file = tokio::fs::File::create(&path).await.unwrap();
338 file.write_all(&file_bytes).await.unwrap();
339
340 let relative_path = path
341 .strip_prefix(&root)
342 .unwrap()
343 .to_string_lossy()
344 .to_string();
345
346 let current = user::Entity::find_by_id(user_id)
347 .one(db).await.unwrap().unwrap();
348
349 let mut model = current.into_active_model();
350 model.profile_picture_path = Set(Some(relative_path.clone()));
351 model.update(db).await.unwrap();
352
353 let response = ProfilePictureResponse {
354 profile_picture_path: relative_path,
355 };
356
357 (
358 StatusCode::OK,
359 Json(ApiResponse::success(response, "Avatar uploaded for user.")),
360 )
361}