headless_lms_server/controllers/
auth.rs1use 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 reqwest::Client;
17use std::{env, time::Duration};
18
19#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
20#[cfg_attr(feature = "ts_rs", derive(TS))]
21pub struct Login {
22 email: String,
23 password: String,
24}
25
26#[instrument(skip(pool, payload,))]
31pub async fn authorize_action_on_resource(
32 pool: web::Data<PgPool>,
33 user: Option<AuthUser>,
34 payload: web::Json<ActionOnResource>,
35) -> ControllerResult<web::Json<bool>> {
36 let mut conn = pool.acquire().await?;
37 let data = payload.0;
38 if let Some(user) = user {
39 match authorize(&mut conn, data.action, Some(user.id), data.resource).await {
40 Ok(true_token) => true_token.authorized_ok(web::Json(true)),
41 _ => {
42 let false_token = skip_authorize();
44 false_token.authorized_ok(web::Json(false))
45 }
46 }
47 } else {
48 let false_token = skip_authorize();
50 false_token.authorized_ok(web::Json(false))
51 }
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55#[cfg_attr(feature = "ts_rs", derive(TS))]
56pub struct CreateAccountDetails {
57 pub email: String,
58 pub first_name: String,
59 pub last_name: String,
60 pub language: String,
61 pub password: String,
62 pub password_confirmation: String,
63 pub country: String,
64}
65
66#[instrument(skip(session, pool, client, payload))]
86pub async fn signup(
87 session: Session,
88 payload: web::Json<CreateAccountDetails>,
89 pool: web::Data<PgPool>,
90 client: web::Data<OAuthClient>,
91 user: Option<AuthUser>,
92) -> ControllerResult<HttpResponse> {
93 if user.is_none() {
94 let user_details = payload.0;
96 post_new_user_to_moocfi(&user_details).await?;
97
98 let mut conn = pool.acquire().await?;
99 let auth_result = authorization::authenticate_moocfi_user(
100 &mut conn,
101 &client,
102 user_details.email,
103 user_details.password,
104 )
105 .await?;
106
107 if let Some((user, _token)) = auth_result {
108 let country = user_details.country.clone();
109 models::user_details::update_user_country(&mut conn, user.id, &country).await?;
110
111 let token = skip_authorize();
112 authorization::remember(&session, user)?;
113 token.authorized_ok(HttpResponse::Ok().finish())
114 } else {
115 Err(ControllerError::new(
116 ControllerErrorType::Unauthorized,
117 "Incorrect email or password.".to_string(),
118 None,
119 ))
120 }
121 } else {
122 Err(ControllerError::new(
123 ControllerErrorType::BadRequest,
124 "Cannot create a new account when signed in.".to_string(),
125 None,
126 ))
127 }
128}
129
130#[instrument(skip(pool, payload,))]
136pub async fn authorize_multiple_actions_on_resources(
137 pool: web::Data<PgPool>,
138 user: Option<AuthUser>,
139 payload: web::Json<Vec<ActionOnResource>>,
140) -> ControllerResult<web::Json<Vec<bool>>> {
141 let mut conn = pool.acquire().await?;
142 let input = payload.into_inner();
143 let mut results = Vec::with_capacity(input.len());
144 if let Some(user) = user {
145 let user_roles = models::roles::get_roles(&mut conn, user.id).await?;
147
148 for action_on_resource in input {
149 if (authorize_with_fetched_list_of_roles(
150 &mut conn,
151 action_on_resource.action,
152 Some(user.id),
153 action_on_resource.resource,
154 &user_roles,
155 )
156 .await)
157 .is_ok()
158 {
159 results.push(true);
160 } else {
161 results.push(false);
162 }
163 }
164 } else {
165 for _action_on_resource in input {
167 results.push(false);
168 }
169 }
170 let token = skip_authorize();
171 token.authorized_ok(web::Json(results))
172}
173
174#[instrument(skip(session, pool, client, payload, app_conf))]
179pub async fn login(
180 session: Session,
181 pool: web::Data<PgPool>,
182 client: web::Data<OAuthClient>,
183 app_conf: web::Data<ApplicationConfiguration>,
184 payload: web::Json<Login>,
185) -> ControllerResult<web::Json<bool>> {
186 let mut conn = pool.acquire().await?;
187 let Login { email, password } = payload.into_inner();
188
189 if app_conf.development_uuid_login {
190 warn!("Trying development mode UUID login");
191 if let Ok(id) = Uuid::parse_str(&email) {
192 let user = { models::users::get_by_id(&mut conn, id).await? };
193 let token = skip_authorize();
194 authorization::remember(&session, user)?;
195 return token.authorized_ok(web::Json(true));
196 };
197 }
198
199 let success = if app_conf.test_mode {
200 warn!("Using test credentials. Normal accounts won't work.");
201 let success =
202 authorization::authenticate_test_user(&mut conn, &email, &password, &app_conf)
203 .await
204 .map_err(|e| {
205 ControllerError::new(
206 ControllerErrorType::Unauthorized,
207 "Could not find the test user. Have you seeded the database?".to_string(),
208 e,
209 )
210 })?;
211 if success {
212 let user = models::users::get_by_email(&mut conn, &email).await?;
213 authorization::remember(&session, user)?;
214 }
215 success
216 } else {
217 let auth_result =
218 authorization::authenticate_moocfi_user(&mut conn, &client, email, password).await?;
219
220 if let Some((user, _token)) = auth_result {
221 authorization::remember(&session, user)?;
222 true
223 } else {
224 false
225 }
226 };
227
228 if success {
229 info!("Authentication successful");
230 } else {
231 warn!("Authentication failed");
232 }
233
234 let token = skip_authorize();
235 token.authorized_ok(web::Json(success))
236}
237
238#[instrument(skip(session))]
242#[allow(clippy::async_yields_async)]
243pub async fn logout(session: Session) -> HttpResponse {
244 authorization::forget(&session);
245 HttpResponse::Ok().finish()
246}
247
248#[instrument(skip(session))]
252pub async fn logged_in(session: Session, pool: web::Data<PgPool>) -> web::Json<bool> {
253 let logged_in = authorization::has_auth_user_session(&session, pool).await;
254 web::Json(logged_in)
255}
256
257#[derive(Debug, Serialize)]
261#[cfg_attr(feature = "ts_rs", derive(TS))]
262pub struct UserInfo {
263 pub user_id: Uuid,
264 pub first_name: Option<String>,
265 pub last_name: Option<String>,
266}
267
268#[instrument(skip(auth_user, pool))]
273pub async fn user_info(
274 auth_user: Option<AuthUser>,
275 pool: web::Data<PgPool>,
276) -> ControllerResult<web::Json<Option<UserInfo>>> {
277 let token = skip_authorize();
278 if let Some(auth_user) = auth_user {
279 let mut conn = pool.acquire().await?;
280 let user_details =
281 models::user_details::get_user_details_by_user_id(&mut conn, auth_user.id).await?;
282
283 token.authorized_ok(web::Json(Some(UserInfo {
284 user_id: user_details.user_id,
285 first_name: user_details.first_name,
286 last_name: user_details.last_name,
287 })))
288 } else {
289 token.authorized_ok(web::Json(None))
290 }
291}
292
293pub async fn post_new_user_to_moocfi(user_details: &CreateAccountDetails) -> anyhow::Result<()> {
297 let tmc_api_url = "https://tmc.mooc.fi/api/v8";
298 let origin = env::var("TMC_ACCOUNT_CREATION_ORIGIN")
299 .expect("TMC_ACCOUNT_CREATION_ORIGIN must be defined");
300 let ratelimit_api_key = env::var("RATELIMIT_PROTECTION_SAFE_API_KEY")
301 .expect("RATELIMIT_PROTECTION_SAFE_API_KEY must be defined");
302 let tmc_client = Client::default();
303 let json = serde_json::json!({
304 "user": {
305 "email": user_details.email,
306 "first_name": user_details.first_name,
307 "last_name": user_details.last_name,
308 "password": user_details.password,
309 "password_confirmation": user_details.password_confirmation
310 },
311 "user_field": {
312 "first_name": user_details.first_name,
313 "last_name": user_details.last_name
314 },
315 "origin": origin,
316 "language": user_details.language
317 });
318 let res = tmc_client
319 .post(format!("{}/users", tmc_api_url))
320 .header("RATELIMIT-PROTECTION-SAFE-API-KEY", ratelimit_api_key)
321 .header(reqwest::header::CONTENT_TYPE, "application/json")
322 .header(reqwest::header::ACCEPT, "application/json")
323 .json(&json)
324 .send()
325 .await
326 .context("Failed to send request to https://tmc.mooc.fi")?;
327 if res.status().is_success() {
328 Ok(())
329 } else {
330 Err(anyhow::anyhow!("Failed to get current user from Mooc.fi"))
331 }
332}
333
334pub fn _add_routes(cfg: &mut ServiceConfig) {
335 cfg.service(
336 web::resource("/signup")
337 .wrap(build_rate_limiting_middleware(Duration::from_secs(60), 15))
338 .wrap(build_rate_limiting_middleware(
339 Duration::from_secs(60 * 60 * 24),
340 1000,
341 ))
342 .to(signup),
343 )
344 .service(
345 web::resource("/login")
346 .wrap(build_rate_limiting_middleware(Duration::from_secs(60), 20))
347 .wrap(build_rate_limiting_middleware(
348 Duration::from_secs(60 * 60),
349 100,
350 ))
351 .wrap(build_rate_limiting_middleware(
352 Duration::from_secs(60 * 60 * 24),
353 500,
354 ))
355 .to(login),
356 )
357 .route("/logout", web::post().to(logout))
358 .route("/logged-in", web::get().to(logged_in))
359 .route("/authorize", web::post().to(authorize_action_on_resource))
360 .route(
361 "/authorize-multiple",
362 web::post().to(authorize_multiple_actions_on_resources),
363 )
364 .route("/user-info", web::get().to(user_info));
365}