Skip to main content

headless_lms_server/controllers/course_material/
user_details.rs

1use std::net::IpAddr;
2
3use headless_lms_utils::ip_to_country::IpToCountryMapper;
4use models::user_details::UserDetail;
5use utoipa::{OpenApi, ToSchema};
6
7use crate::prelude::*;
8
9#[derive(OpenApi)]
10#[openapi(paths(get_user_details, update_user_info, get_user_country_by_ip))]
11pub(crate) struct CourseMaterialUserDetailsApiDoc;
12
13/**
14GET `/api/v0/course-material/user-details/user` - Find user details by user id
15*/
16#[utoipa::path(
17    get,
18    path = "/user",
19    operation_id = "getCourseMaterialAuthenticatedUserDetails",
20    tag = "course-material-user-details",
21    responses(
22        (status = 200, description = "Authenticated user details", body = UserDetail)
23    )
24)]
25#[instrument(skip(pool))]
26pub async fn get_user_details(
27    user: AuthUser,
28    pool: web::Data<PgPool>,
29) -> ControllerResult<web::Json<UserDetail>> {
30    let mut conn = pool.acquire().await?;
31
32    let token = skip_authorize();
33
34    let res = models::user_details::get_user_details_by_user_id(&mut conn, user.id).await?;
35    token.authorized_ok(web::Json(res))
36}
37
38#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
39
40pub struct UserInfoPayload {
41    pub email: String,
42    pub first_name: String,
43    pub last_name: String,
44    pub country: String,
45    pub email_communication_consent: bool,
46}
47
48/**
49POST `/api/v0/course-material/user-details/update-user-info` - Updates the users information such as email, name, country and email communication consent
50*/
51#[utoipa::path(
52    post,
53    path = "/update-user-info",
54    operation_id = "updateCourseMaterialUserInfo",
55    tag = "course-material-user-details",
56    request_body = UserInfoPayload,
57    responses(
58        (status = 200, description = "Updated user details", body = UserDetail)
59    )
60)]
61#[instrument(skip(pool, app_conf))]
62pub async fn update_user_info(
63    user: AuthUser,
64    pool: web::Data<PgPool>,
65    payload: web::Json<UserInfoPayload>,
66    app_conf: web::Data<ApplicationConfiguration>,
67) -> ControllerResult<web::Json<UserDetail>> {
68    let mut conn = pool.acquire().await?;
69    let existing = models::user_details::get_user_details_by_user_id(&mut conn, user.id).await?;
70    let res = models::user_details::update_user_info(
71        &mut conn,
72        user.id,
73        &payload.email,
74        &payload.first_name,
75        &payload.last_name,
76        &payload.country,
77        payload.email_communication_consent,
78    )
79    .await?;
80
81    if existing.email != res.email {
82        // A database trigger has already cleared `email_verified_at` for the old address.
83        domain::email_ownership_verification::queue_verification_email_best_effort(
84            &mut conn,
85            app_conf.enable_email_ownership_verification,
86            user.id,
87        )
88        .await;
89    }
90
91    let token = skip_authorize();
92    token.authorized_ok(web::Json(res))
93}
94
95/**
96GET `/api/v0/course-material/user-details/users-ip-country` - Find users country by their IP  address
97*/
98#[utoipa::path(
99    get,
100    path = "/users-ip-country",
101    operation_id = "getCourseMaterialCountryFromIp",
102    tag = "course-material-user-details",
103    responses(
104        (status = 200, description = "Detected country code", body = String)
105    )
106)]
107pub async fn get_user_country_by_ip(
108    req: HttpRequest,
109    ip_to_country_mapper: web::Data<IpToCountryMapper>,
110) -> ControllerResult<String> {
111    let connection_info = req.connection_info();
112
113    let ip: Option<IpAddr> = connection_info
114        .realip_remote_addr()
115        .and_then(|ip| ip.parse::<IpAddr>().ok());
116
117    let country = ip
118        .and_then(|ip| ip_to_country_mapper.map_ip_to_country(&ip))
119        .map(|c| c.to_string())
120        .unwrap_or_default();
121
122    let token = skip_authorize();
123    token.authorized_ok(country.to_string())
124}
125
126/**
127Add a route for each controller in this module.
128
129The name starts with an underline in order to appear before other functions in the module documentation.
130
131We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
132*/
133pub fn _add_routes(cfg: &mut ServiceConfig) {
134    cfg.route("/user", web::get().to(get_user_details))
135        .route("/update-user-info", web::post().to(update_user_info))
136        .route("/users-ip-country", web::get().to(get_user_country_by_ip));
137}