1use crate::{
6 OAuthClient,
7 domain::{
8 authorization::{
9 self, ActionOnResource, authorize_with_fetched_list_of_roles, skip_authorize,
10 },
11 rate_limit_middleware_builder::build_rate_limiting_middleware,
12 },
13 prelude::*,
14};
15use actix_session::Session;
16use anyhow::Error;
17use anyhow::anyhow;
18use headless_lms_utils::tmc::{NewUserInfo, TmcClient};
19use std::time::Duration;
20use tracing_log::log;
21
22#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
23#[cfg_attr(feature = "ts_rs", derive(TS))]
24pub struct Login {
25 email: String,
26 password: String,
27}
28
29#[instrument(skip(pool, payload,))]
34pub async fn authorize_action_on_resource(
35 pool: web::Data<PgPool>,
36 user: Option<AuthUser>,
37 payload: web::Json<ActionOnResource>,
38) -> ControllerResult<web::Json<bool>> {
39 let mut conn = pool.acquire().await?;
40 let data = payload.0;
41 if let Some(user) = user {
42 match authorize(&mut conn, data.action, Some(user.id), data.resource).await {
43 Ok(true_token) => true_token.authorized_ok(web::Json(true)),
44 _ => {
45 let false_token = skip_authorize();
47 false_token.authorized_ok(web::Json(false))
48 }
49 }
50 } else {
51 let false_token = skip_authorize();
53 false_token.authorized_ok(web::Json(false))
54 }
55}
56
57#[derive(Debug, Serialize, Deserialize)]
58#[cfg_attr(feature = "ts_rs", derive(TS))]
59pub struct CreateAccountDetails {
60 pub email: String,
61 pub first_name: String,
62 pub last_name: String,
63 pub language: String,
64 pub password: String,
65 pub password_confirmation: String,
66 pub country: String,
67 pub email_communication_consent: bool,
68}
69
70#[instrument(skip(session, pool, client, payload, app_conf))]
91pub async fn signup(
92 session: Session,
93 payload: web::Json<CreateAccountDetails>,
94 pool: web::Data<PgPool>,
95 client: web::Data<OAuthClient>,
96 user: Option<AuthUser>,
97 app_conf: web::Data<ApplicationConfiguration>,
98 tmc_client: web::Data<TmcClient>,
99) -> ControllerResult<HttpResponse> {
100 let user_details = payload.0;
101 let mut conn = pool.acquire().await?;
102
103 if app_conf.test_mode {
104 return handle_test_mode_signup(&mut conn, &session, &user_details, &app_conf).await;
105 }
106
107 if user.is_none() {
108 post_new_user_to_moocfi(&user_details, tmc_client, &app_conf).await?;
110
111 let auth_result = authorization::authenticate_moocfi_user(
112 &mut conn,
113 &client,
114 user_details.email,
115 user_details.password,
116 )
117 .await?;
118
119 if let Some((user, _token)) = auth_result {
120 let country = user_details.country.clone();
121 models::user_details::update_user_country(&mut conn, user.id, &country).await?;
122 models::user_details::update_user_email_communication_consent(
123 &mut conn,
124 user.id,
125 user_details.email_communication_consent,
126 )
127 .await?;
128
129 let token = skip_authorize();
130 authorization::remember(&session, user)?;
131 token.authorized_ok(HttpResponse::Ok().finish())
132 } else {
133 Err(ControllerError::new(
134 ControllerErrorType::Unauthorized,
135 "Incorrect email or password.".to_string(),
136 None,
137 ))
138 }
139 } else {
140 Err(ControllerError::new(
141 ControllerErrorType::BadRequest,
142 "Cannot create a new account when signed in.".to_string(),
143 None,
144 ))
145 }
146}
147
148async fn handle_test_mode_signup(
149 conn: &mut PgConnection,
150 session: &Session,
151 user_details: &CreateAccountDetails,
152 app_conf: &ApplicationConfiguration,
153) -> ControllerResult<HttpResponse> {
154 assert!(
155 app_conf.test_mode,
156 "handle_test_mode_signup called outside test mode"
157 );
158
159 warn!("Handling signup in test mode. No real account is created.");
160
161 let user_id = models::users::insert(
162 conn,
163 PKeyPolicy::Generate,
164 &user_details.email,
165 Some(&user_details.first_name),
166 Some(&user_details.last_name),
167 )
168 .await
169 .map_err(|e| {
170 ControllerError::new(
171 ControllerErrorType::InternalServerError,
172 "Failed to insert test user.".to_string(),
173 Some(anyhow!(e)),
174 )
175 })?;
176
177 models::user_details::update_user_country(conn, user_id, &user_details.country).await?;
178 models::user_details::update_user_email_communication_consent(
179 conn,
180 user_id,
181 user_details.email_communication_consent,
182 )
183 .await?;
184
185 let user = models::users::get_by_email(conn, &user_details.email).await?;
186 authorization::remember(session, user)?;
187
188 let token = skip_authorize();
189 token.authorized_ok(HttpResponse::Ok().finish())
190}
191
192#[instrument(skip(pool, payload,))]
198pub async fn authorize_multiple_actions_on_resources(
199 pool: web::Data<PgPool>,
200 user: Option<AuthUser>,
201 payload: web::Json<Vec<ActionOnResource>>,
202) -> ControllerResult<web::Json<Vec<bool>>> {
203 let mut conn = pool.acquire().await?;
204 let input = payload.into_inner();
205 let mut results = Vec::with_capacity(input.len());
206 if let Some(user) = user {
207 let user_roles = models::roles::get_roles(&mut conn, user.id).await?;
209
210 for action_on_resource in input {
211 if (authorize_with_fetched_list_of_roles(
212 &mut conn,
213 action_on_resource.action,
214 Some(user.id),
215 action_on_resource.resource,
216 &user_roles,
217 )
218 .await)
219 .is_ok()
220 {
221 results.push(true);
222 } else {
223 results.push(false);
224 }
225 }
226 } else {
227 for _action_on_resource in input {
229 results.push(false);
230 }
231 }
232 let token = skip_authorize();
233 token.authorized_ok(web::Json(results))
234}
235
236#[instrument(skip(session, pool, client, payload, app_conf))]
241pub async fn login(
242 session: Session,
243 pool: web::Data<PgPool>,
244 client: web::Data<OAuthClient>,
245 app_conf: web::Data<ApplicationConfiguration>,
246 payload: web::Json<Login>,
247) -> ControllerResult<web::Json<bool>> {
248 let mut conn = pool.acquire().await?;
249 let Login { email, password } = payload.into_inner();
250
251 if app_conf.development_uuid_login {
252 warn!("Trying development mode UUID login");
253 if let Ok(id) = Uuid::parse_str(&email) {
254 let user = { models::users::get_by_id(&mut conn, id).await? };
255 let token = skip_authorize();
256 authorization::remember(&session, user)?;
257 return token.authorized_ok(web::Json(true));
258 };
259 }
260
261 let success = if app_conf.test_mode {
262 warn!("Using test credentials. Normal accounts won't work.");
263 let success =
264 authorization::authenticate_test_user(&mut conn, &email, &password, &app_conf)
265 .await
266 .map_err(|e| {
267 ControllerError::new(
268 ControllerErrorType::Unauthorized,
269 "Could not find the test user. Have you seeded the database?".to_string(),
270 e,
271 )
272 })?;
273 if success {
274 let user = models::users::get_by_email(&mut conn, &email).await?;
275 authorization::remember(&session, user)?;
276 }
277 success
278 } else {
279 let auth_result =
280 authorization::authenticate_moocfi_user(&mut conn, &client, email, password).await?;
281
282 if let Some((user, _token)) = auth_result {
283 authorization::remember(&session, user)?;
284 true
285 } else {
286 false
287 }
288 };
289
290 if success {
291 info!("Authentication successful");
292 } else {
293 warn!("Authentication failed");
294 }
295
296 let token = skip_authorize();
297 token.authorized_ok(web::Json(success))
298}
299
300#[instrument(skip(session))]
304#[allow(clippy::async_yields_async)]
305pub async fn logout(session: Session) -> HttpResponse {
306 authorization::forget(&session);
307 HttpResponse::Ok().finish()
308}
309
310#[instrument(skip(session))]
314pub async fn logged_in(session: Session, pool: web::Data<PgPool>) -> web::Json<bool> {
315 let logged_in = authorization::has_auth_user_session(&session, pool).await;
316 web::Json(logged_in)
317}
318
319#[derive(Debug, Serialize)]
323#[cfg_attr(feature = "ts_rs", derive(TS))]
324pub struct UserInfo {
325 pub user_id: Uuid,
326 pub first_name: Option<String>,
327 pub last_name: Option<String>,
328}
329
330#[instrument(skip(auth_user, pool))]
335pub async fn user_info(
336 auth_user: Option<AuthUser>,
337 pool: web::Data<PgPool>,
338) -> ControllerResult<web::Json<Option<UserInfo>>> {
339 let token = skip_authorize();
340 if let Some(auth_user) = auth_user {
341 let mut conn = pool.acquire().await?;
342 let user_details =
343 models::user_details::get_user_details_by_user_id(&mut conn, auth_user.id).await?;
344
345 token.authorized_ok(web::Json(Some(UserInfo {
346 user_id: user_details.user_id,
347 first_name: user_details.first_name,
348 last_name: user_details.last_name,
349 })))
350 } else {
351 token.authorized_ok(web::Json(None))
352 }
353}
354
355pub async fn post_new_user_to_moocfi(
359 user_details: &CreateAccountDetails,
360 tmc_client: web::Data<TmcClient>,
361 app_conf: &ApplicationConfiguration,
362) -> anyhow::Result<()> {
363 tmc_client
364 .post_new_user_to_moocfi(
365 NewUserInfo {
366 first_name: user_details.first_name.clone(),
367 last_name: user_details.last_name.clone(),
368 email: user_details.email.clone(),
369 password: user_details.password.clone(),
370 password_confirmation: user_details.password_confirmation.clone(),
371 language: user_details.language.clone(),
372 },
373 app_conf,
374 )
375 .await
376}
377
378pub async fn update_user_information_to_tmc(
379 first_name: String,
380 last_name: String,
381 email: Option<String>,
382 user_upstream_id: String,
383 tmc_client: web::Data<TmcClient>,
384 app_conf: web::Data<ApplicationConfiguration>,
385) -> Result<(), Error> {
386 if app_conf.test_mode {
387 return Ok(());
388 }
389 tmc_client
390 .update_user_information(first_name, last_name, email, user_upstream_id)
391 .await
392 .map_err(|e| {
393 log::warn!("TMC user update failed: {:?}", e);
394 anyhow::anyhow!("TMC user update failed: {}", e)
395 })?;
396 Ok(())
397}
398
399pub fn _add_routes(cfg: &mut ServiceConfig) {
400 cfg.service(
401 web::resource("/signup")
402 .wrap(build_rate_limiting_middleware(Duration::from_secs(60), 15))
403 .wrap(build_rate_limiting_middleware(
404 Duration::from_secs(60 * 60 * 24),
405 1000,
406 ))
407 .to(signup),
408 )
409 .service(
410 web::resource("/login")
411 .wrap(build_rate_limiting_middleware(Duration::from_secs(60), 20))
412 .wrap(build_rate_limiting_middleware(
413 Duration::from_secs(60 * 60),
414 100,
415 ))
416 .wrap(build_rate_limiting_middleware(
417 Duration::from_secs(60 * 60 * 24),
418 500,
419 ))
420 .to(login),
421 )
422 .route("/logout", web::post().to(logout))
423 .route("/logged-in", web::get().to(logged_in))
424 .route("/authorize", web::post().to(authorize_action_on_resource))
425 .route(
426 "/authorize-multiple",
427 web::post().to(authorize_multiple_actions_on_resources),
428 )
429 .route("/user-info", web::get().to(user_info));
430}