api/routes/auth/
get.rs

1use std::path::PathBuf;
2use axum::{
3    extract::{State, Query, Path},
4    http::{StatusCode, header, HeaderMap, HeaderValue},
5    response::IntoResponse,
6    Json,
7};
8use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
9use tokio::fs::File as FsFile;
10use serde::{Deserialize, Serialize};
11use tokio::io::AsyncReadExt;
12use util::state::AppState;
13use crate::{
14    auth::claims::AuthUser,
15    response::ApiResponse,
16};
17use db::models::{user, module, user_module_role, user_module_role::Role};
18use crate::routes::common::UserModule;
19
20#[derive(Debug, Serialize)]
21pub struct MeResponse {
22    pub id: i64,
23    pub email: String,
24    pub username: String,
25    pub admin: bool,
26    pub created_at: String,
27    pub updated_at: String,
28    pub modules: Vec<UserModule>,
29}
30
31#[derive(Deserialize)]
32pub struct HasRoleQuery {
33    pub module_id: i64,
34    pub role: String,
35}
36
37#[derive(serde::Serialize)]
38pub struct HasRoleResponse {
39    pub has_role: bool,
40}
41
42/// GET /api/auth/me
43///
44/// Returns the authenticated user's profile along with their module roles.
45/// Requires a valid bearer token in the `Authorization` header.
46///
47/// ### Response: 200 OK
48/// ```json
49/// {
50///   "success": true,
51///   "message": "User data retrieved successfully",
52///   "data": {
53///     "id": 42,
54///     "email": "[email protected]",
55///     "username": null,
56///     "admin": true,
57///     "created_at": "2024-11-10T12:34:56Z",
58///     "updated_at": "2025-06-18T10:00:00Z",
59///     "modules": [
60///       {
61///         "module_id": 101,
62///         "module_code": "CS101",
63///         "module_year": 2025,
64///         "module_description": "Intro to Computer Science",
65///         "module_credits": 15,
66///         "module_created_at": "2023-11-01T08:00:00Z",
67///         "module_updated_at": "2025-02-20T14:22:00Z",
68///         "role": "Lecturer"
69///       },
70///       {
71///         "module_id": 202,
72///         "module_code": "CS202",
73///         "module_year": 2025,
74///         "module_description": "Data Structures",
75///         "module_credits": 20,
76///         "module_created_at": "2023-11-05T09:00:00Z",
77///         "module_updated_at": "2025-03-10T13:45:00Z",
78///         "role": "Admin"
79///       }
80///     ]
81///   }
82/// }
83/// ```
84///
85/// ### Error Responses
86/// - `403 Forbidden` – Missing or invalid token
87/// - `404 Not Found` – User not found
88/// - `500 Internal Server Error` – Database failure
89pub async fn get_me(
90    State(app_state): State<AppState>,
91    AuthUser(claims): AuthUser
92) -> impl IntoResponse {
93    let db = app_state.db();
94    let user_id = claims.sub;
95
96    let user = match user::Entity::find()
97        .filter(user::Column::Id.eq(user_id))
98        .one(db)
99        .await
100    {
101        Ok(Some(u)) => u,
102        Ok(None) => {
103            return (
104                StatusCode::NOT_FOUND,
105                Json(ApiResponse::<MeResponse>::error("User not found")),
106            );
107        }
108        Err(_) => {
109            return (
110                StatusCode::INTERNAL_SERVER_ERROR,
111                Json(ApiResponse::<MeResponse>::error("Database error")),
112            );
113        }
114    };
115    
116    let roles = match user_module_role::Entity::find()
117        .filter(user_module_role::Column::UserId.eq(user.id))
118        .find_also_related(module::Entity)
119        .all(db)
120        .await
121    {
122        Ok(results) => results,
123        Err(_) => {
124            return (
125                StatusCode::INTERNAL_SERVER_ERROR,
126                Json(ApiResponse::<MeResponse>::error("Failed to load module roles")),
127            );
128        }
129    };
130
131    let modules: Vec<UserModule> = roles
132        .into_iter()
133        .filter_map(|(role, maybe_module)| {
134            maybe_module.map(|m| UserModule {
135                id: m.id,
136                code: m.code,
137                year: m.year,
138                description: m.description.unwrap_or_default(),
139                credits: m.credits,
140                created_at: m.created_at.to_rfc3339(),
141                updated_at: m.updated_at.to_rfc3339(),
142                role: role.role.to_string(),
143            })
144        })
145        .collect();
146
147    let response_data = MeResponse {
148        id: user.id,
149        email: user.email,
150        username: user.username,
151        admin: user.admin,
152        created_at: user.created_at.to_rfc3339(),
153        updated_at: user.updated_at.to_rfc3339(),
154        modules,
155    };
156
157    (
158        StatusCode::OK,
159        Json(ApiResponse::success(response_data, "User data retrieved successfully")),
160    )
161}
162
163/// GET /api/auth/avatar/{user_id}
164///
165/// Returns the avatar image for a specific user ID.
166///
167/// ### Authorization
168/// This endpoint is **public** and does **not** require authentication.
169///
170/// ### Response: 200 OK
171/// - Returns raw binary image data of the avatar
172/// - The `Content-Type` header is automatically inferred based on the file extension (e.g., `image/png`, `image/jpeg`)
173///
174/// ### Example Request
175/// ```http
176/// GET /api/auth/avatar/42
177/// ```
178///
179/// ### Example Response Headers
180/// ```http
181/// Content-Type: image/png
182/// ```
183///
184/// ### Error Responses
185/// #### 404 Not Found
186/// - User does not exist
187/// - No avatar is set
188/// - Avatar file is missing
189/// ```json
190/// {
191///   "success": false,
192///   "message": "No avatar set",
193///   "data": null
194/// }
195/// ```
196///
197/// #### 500 Internal Server Error
198/// - Database error
199/// - File could not be opened or read
200/// ```json
201/// {
202///   "success": false,
203///   "message": "Failed to read avatar",
204///   "data": null
205/// }
206/// ```
207pub async fn get_avatar(
208    State(app_state): State<AppState>,
209    Path(user_id): Path<i64>
210) -> impl IntoResponse {
211    let db = app_state.db();
212
213    let user = user::Entity::find_by_id(user_id).one(db).await.unwrap().unwrap();
214
215    let Some(path) = user.profile_picture_path else {
216        return (
217            StatusCode::NOT_FOUND,
218            Json(ApiResponse::<()>::error("No avatar set")),
219        )
220            .into_response();
221    };
222
223    let root = std::env::var("USER_PROFILE_STORAGE_ROOT")
224        .unwrap_or_else(|_| "data/user_profile_pictures".to_string());
225    let fs_path = PathBuf::from(root).join(path);
226
227    if tokio::fs::metadata(&fs_path).await.is_err() {
228        return (
229            StatusCode::NOT_FOUND,
230            Json(ApiResponse::<()>::error("Avatar file missing")),
231        )
232            .into_response();
233    }
234
235    let mut file = match FsFile::open(&fs_path).await {
236        Ok(f) => f,
237        Err(_) => {
238            return (
239                StatusCode::INTERNAL_SERVER_ERROR,
240                Json(ApiResponse::<()>::error("Could not open avatar")),
241            )
242                .into_response();
243        }
244    };
245
246    let mut buffer = Vec::new();
247    if let Err(_) = file.read_to_end(&mut buffer).await {
248        return (
249            StatusCode::INTERNAL_SERVER_ERROR,
250            Json(ApiResponse::<()>::error("Failed to read avatar")),
251        )
252            .into_response();
253    }
254
255    let mime = mime_guess::from_path(&fs_path)
256        .first_or_octet_stream()
257        .to_string();
258
259    let mut headers = HeaderMap::new();
260    headers.insert(
261        header::CONTENT_TYPE,
262        HeaderValue::from_str(&mime).unwrap_or(HeaderValue::from_static("application/octet-stream")),
263    );
264
265    (StatusCode::OK, headers, buffer).into_response()
266}
267
268/// GET /api/auth/has-role
269///
270/// Checks if the authenticated user has a specific role in a module.
271///
272/// ### Authorization
273/// This endpoint requires a valid bearer token in the `Authorization` header.
274///
275/// ### Request Parameters
276/// - `module_id`: The ID of the module to check role for
277/// - `role`: The role to check for (case-insensitive: "lecturer", "tutor", "student")
278///
279/// ### Response: 200 OK
280/// ```json
281/// {
282///   "success": true,
283///   "message": "Role check completed",
284///   "data": {
285///     "has_role": true
286/// }
287/// ```
288///
289/// ### Error Responses
290/// - `400 Bad Request` – Invalid role specified
291/// - `403 Forbidden` – Missing or invalid token
292/// - `500 Internal Server Error` – Database failure
293pub async fn has_role_in_module(
294    State(app_state): State<AppState>,
295    AuthUser(claims): AuthUser,
296    Query(params): Query<HasRoleQuery>
297) -> impl IntoResponse {
298    let db = app_state.db();
299
300    let role = match params.role.to_lowercase().as_str() {
301        "lecturer" => Role::Lecturer,
302        "assistant_lecturer" => Role::AssistantLecturer,
303        "tutor" => Role::Tutor,
304        "student" => Role::Student,
305        _ => {
306            return (
307                StatusCode::BAD_REQUEST,
308                Json(ApiResponse::<HasRoleResponse>::error("Invalid role specified")),
309            )
310        }
311    };
312
313    let exists = match user_module_role::Entity::find()
314        .filter(user_module_role::Column::UserId.eq(claims.sub))
315        .filter(user_module_role::Column::ModuleId.eq(params.module_id))
316        .filter(user_module_role::Column::Role.eq(role))
317        .one(db)
318        .await
319    {
320        Ok(Some(_)) => true,
321        Ok(None) => false,
322        Err(_) => {
323            return (
324                StatusCode::INTERNAL_SERVER_ERROR,
325                Json(ApiResponse::<HasRoleResponse>::error("Database error")),
326            )
327        }
328    };
329
330    let response = HasRoleResponse { has_role: exists };
331
332    (
333        StatusCode::OK,
334        Json(ApiResponse::success(response, "Role check completed")),
335    )
336}
337
338#[derive(Debug, Deserialize)]
339pub struct ModuleRoleQuery {
340    pub module_id: i32,
341}
342
343#[derive(Debug, Serialize)]
344pub struct ModuleRoleResponse {
345    pub role: Option<String>,
346}
347
348/// GET /api/auth/module-role
349///
350/// Returns the role of the authenticated user in the given module, if any.
351///
352/// ### Authorization
353/// Requires a valid bearer token.
354///
355/// ### Query Parameters
356/// - `module_id`: The module ID to check
357///
358/// ### Response
359/// ```json
360/// {
361///   "success": true,
362///   "message": "Role fetched successfully",
363///   "data": {
364///     "role": "lecturer" // or "tutor", "student", null
365///   }
366/// }
367/// ```
368/// #[derive(Debug, Deserialize)]
369pub async fn get_module_role(
370    State(app_state): State<AppState>,
371    AuthUser(claims): AuthUser,
372    Query(params): Query<ModuleRoleQuery>,
373) -> impl IntoResponse {
374    let db = app_state.db();
375
376    let role = match user_module_role::Entity::find()
377        .filter(user_module_role::Column::UserId.eq(claims.sub))
378        .filter(user_module_role::Column::ModuleId.eq(params.module_id))
379        .one(db)
380        .await
381    {
382        Ok(Some(model)) => Some(model.role.to_string().to_lowercase()),
383        Ok(None) => None,
384        Err(_) => {
385            return (
386                StatusCode::INTERNAL_SERVER_ERROR,
387                Json(ApiResponse::<ModuleRoleResponse>::error("Database error")),
388            );
389        }
390    };
391
392    (
393        StatusCode::OK,
394        Json(ApiResponse::success(
395            ModuleRoleResponse { role },
396            "Role fetched successfully",
397        )),
398    )
399}