1#[cfg(feature = "plugin_utoipa")]
2use crate::auth::{
3 AuthMessageResponse, AuthTokenResponse, JwtSecurityAddon, UserSessionJson, UserSessionResponse,
4};
5use actix_http::StatusCode;
6use actix_web::cookie::{Cookie, SameSite};
7use actix_web::{delete, get, post, web, Error as AWError, Result};
8use actix_web::{
9 web::{Data, Json, Path, Query},
10 HttpRequest, HttpResponse,
11};
12use serde_json::json;
13#[cfg(feature = "plugin_utoipa")]
14use utoipa::OpenApi;
15
16use crate::auth::{
17 controller,
18 controller::{
19 ActivationInput, ChangeInput, ForgotInput, LoginInput, RegisterInput, ResetInput,
20 COOKIE_NAME,
21 },
22 Auth, PaginationParams, ID,
23};
24use crate::{auth::AuthConfig, AppConfig, Database, Mailer};
25
26#[cfg_attr(feature = "plugin_utoipa", utoipa::path(
38 context_path = "/api/auth",
39 params(PaginationParams),
40 responses(
41 (status = 200, description = "success, returns a json payload with all the sessions belonging to the authenticated user", body = UserSessionResponse),
42 (status = 401, description = "Error: Unauthorized"),
43 (status = 500, description = "Could not fetch sessions."),
44 ),
45 tag = "Sessions",
46 security ( ("JWT" = []))
47))]
48#[get("/sessions")]
49async fn sessions(
50 db: Data<Database>,
51 auth: Auth,
52 Query(info): Query<PaginationParams>,
53) -> Result<HttpResponse> {
54 let result =
55 web::block(move || controller::get_sessions(db.into_inner().as_ref(), &auth, &info))
56 .await?;
57
58 match result {
59 Ok(sessions) => Ok(HttpResponse::Ok().json(sessions)),
60 Err((status_code, error_message)) => Ok(HttpResponse::build(
61 StatusCode::from_u16(status_code).unwrap(),
62 )
63 .body(json!({ "message": error_message }).to_string())),
64 }
65}
66
67#[cfg_attr(feature = "plugin_utoipa", utoipa::path(
74 context_path = "/api/auth",
75 responses(
76 (status = 200, description = "Deleted", body = AuthMessageResponse),
77 (status = 401, description = "User not authenticated"),
78 (status = 404, description = "User session could not be found, or does not belong to authenticated user.", body = AuthMessageResponse),
79 (status = 500, description = "Internal Error.", body = AuthMessageResponse),
80 (status = 500, description = "Could not delete session.", body = AuthMessageResponse),
81 ),
82 tag = "Sessions",
83 security ( ("JWT" = []))
84))]
85#[delete("/sessions/{id}")]
86async fn destroy_session(
87 db: Data<Database>,
88 item_id: Path<ID>,
89 auth: Auth,
90) -> Result<HttpResponse> {
91 let result =
92 web::block(move || controller::destroy_session(&db, &auth, item_id.into_inner())).await?;
93
94 match result {
95 Ok(()) => Ok(
96 HttpResponse::build(StatusCode::OK).body(json!({"message": "Deleted."}).to_string())
97 ),
98 Err((status_code, error_message)) => Ok(HttpResponse::build(
99 StatusCode::from_u16(status_code).unwrap(),
100 )
101 .body(json!({ "message": error_message }).to_string())),
102 }
103}
104
105#[cfg_attr(feature = "plugin_utoipa", utoipa::path(
112 context_path = "/api/auth",
113 responses(
114 (status = 200, description = "Deleted", body = AuthMessageResponse),
115 (status = 401, description = "User not authenticated"),
116 (status = 500, description = "Could not delete sessions.", body = AuthMessageResponse),
117 ),
118 tag = "Sessions",
119 security ( ("JWT" = []))
120))]
121#[delete("/sessions")]
122async fn destroy_sessions(db: Data<Database>, auth: Auth) -> Result<HttpResponse, AWError> {
123 let result = web::block(move || controller::destroy_sessions(&db, &auth)).await?;
124
125 match result {
126 Ok(()) => Ok(
127 HttpResponse::build(StatusCode::OK).body(json!({"message": "Deleted."}).to_string())
128 ),
129 Err((status_code, error_message)) => Ok(HttpResponse::build(
130 StatusCode::from_u16(status_code).unwrap(),
131 )
132 .body(json!({ "message": error_message }).to_string())),
133 }
134}
135
136#[cfg_attr(feature = "plugin_utoipa", utoipa::path(
141 context_path = "/api/auth",
142 request_body(content = LoginInput, content_type = "application/json"),
143 responses(
144 (status = 200, description = "session created", body = AuthTokenResponse),
145 (status = 400, description = "'device' cannot be longer than 256 characters.", body = AuthMessageResponse),
146 (status = 400, description = "Account has not been activated.", body = AuthMessageResponse),
147 (status = 401, description = "Invalid credentials.", body = AuthMessageResponse),
148 (status = 500, description = "An internal server error occurred.", body = AuthMessageResponse),
149 (status = 500, description = "Could not create a session.", body = AuthMessageResponse),
150 ),
151 tag = "Sessions",
152))]
153#[post("/login")]
154async fn login(db: Data<Database>, Json(item): Json<LoginInput>) -> Result<HttpResponse, AWError> {
155 let result = web::block(move || controller::login(&db, &item)).await?;
156
157 match result {
158 Ok((access_token, refresh_token)) => Ok(HttpResponse::build(StatusCode::OK)
159 .cookie(
160 Cookie::build(COOKIE_NAME, refresh_token)
161 .secure(true)
162 .http_only(true)
163 .same_site(SameSite::Strict)
164 .path("/")
165 .finish(),
166 )
167 .body(json!({ "access_token": access_token }).to_string())),
168 Err((status_code, message)) => Ok(HttpResponse::build(
169 StatusCode::from_u16(status_code).unwrap(),
170 )
171 .body(json!({ "message": message }).to_string())),
172 }
173}
174
175#[cfg(feature = "plugin_auth-oidc")]
176#[get("/oidc/{provider}")]
177async fn oidc_login_redirect(
178 db: Data<Database>,
179 app_config: Data<AppConfig>,
180 auth_config: Data<AuthConfig>,
181 provider: Path<String>,
182) -> Result<HttpResponse, AWError> {
183 use actix_web::http::header::{HeaderValue, LOCATION};
184
185 let result = crate::auth::oidc::controller::oidc_login_url(
186 &db,
187 app_config.as_ref(),
188 auth_config.as_ref(),
189 provider.to_string(),
190 )
191 .await;
192
193 match result {
194 Ok(Some(url)) => {
195 let mut response = HttpResponse::SeeOther().body(());
196 response
197 .headers_mut()
198 .append(LOCATION, HeaderValue::from_str(url.as_str()).unwrap());
199
200 Ok(response)
201 }
202 Ok(None) => Ok(HttpResponse::NotImplemented().finish()),
203 Err(_) => Ok(HttpResponse::InternalServerError().finish()),
204 }
205}
206
207#[cfg(feature = "plugin_auth-oidc")]
208#[derive(serde::Deserialize)]
209pub struct OIDCLoginQueryParams {
210 code: Option<String>,
211 state: Option<String>,
212 error: Option<String>,
213}
214
215#[cfg(feature = "plugin_auth-oidc")]
216#[get("/oidc/{provider}/login")]
217async fn oidc_login(
219 db: Data<Database>,
220 app_config: Data<AppConfig>,
221 auth_config: Data<AuthConfig>,
222 path_params: Path<String>,
223 query_params: Query<OIDCLoginQueryParams>,
224) -> HttpResponse {
225 use actix_web::http::header::{HeaderValue, LOCATION};
226 let provider_name = path_params.to_string();
227
228 let provider = if let Some(provider) = auth_config
229 .oidc_providers
230 .iter()
231 .find(|p| p.name.eq(&provider_name))
232 {
233 provider
234 } else {
235 return HttpResponse::InternalServerError().json(
236 json!({
237 "success": false,
238 "message": "Provider not configured",
239 "provider": &provider_name
240 })
241 .to_string(),
242 );
243 };
244
245 let query_params = query_params.into_inner();
246 let query_param_code = query_params.code;
247 let query_param_state = query_params.state;
248 let query_param_error = query_params.error;
249
250 let resp = crate::auth::oidc::controller::oauth_login(
251 &db,
252 &app_config,
253 &auth_config,
254 provider_name,
255 query_param_code,
256 query_param_error,
257 query_param_state,
258 )
259 .await;
260
261 let mut response = HttpResponse::SeeOther().body(());
262
263 match resp {
264 Ok((access_token, refresh_token)) => {
265 response.headers_mut().append(
266 LOCATION,
267 HeaderValue::from_str(&format!(
268 "{}?access_token={}",
269 provider.success_uri, access_token
270 ))
271 .expect("Invalid URL"),
272 );
273
274 response
275 .add_cookie(
276 &Cookie::build(COOKIE_NAME, refresh_token)
277 .secure(true)
278 .http_only(true)
279 .same_site(SameSite::Strict)
280 .path("/")
281 .finish(),
282 )
283 .expect("Could not add refresh_token cookie");
284 }
285 Err((status_code, message)) => response.headers_mut().append(
286 LOCATION,
287 HeaderValue::from_str(&format!(
288 "{}?status_code={}&message={}",
289 provider.error_uri, status_code, message
290 ))
291 .expect("Invalid URL"),
292 ),
293 }
294
295 response
296}
297
298#[cfg_attr(feature = "plugin_utoipa", utoipa::path(
304 context_path = "/api/auth",
305 responses(
306 (status = 200, description = "deletes the \"refresh_token\" cookie"),
307 (status = 401, description = "Invalid session.", body = AuthMessageResponse),
308 (status = 401, description = "Could not delete session.", body = AuthMessageResponse),
309 ),
310 tag = "Sessions",
311))]
312#[post("/logout")]
313#[allow(clippy::future_not_send)] async fn logout(db: Data<Database>, req: HttpRequest) -> Result<HttpResponse, AWError> {
315 let refresh_token = req
316 .cookie(COOKIE_NAME)
317 .map(|cookie| String::from(cookie.value()));
318
319 let result = web::block(move || {
320 controller::logout(&db, refresh_token.as_ref().map(std::convert::AsRef::as_ref))
321 })
322 .await?;
323
324 match result {
325 Ok(()) => {
326 let mut cookie = Cookie::named(COOKIE_NAME);
327 cookie.make_removal();
328
329 Ok(HttpResponse::Ok().cookie(cookie).finish())
330 }
331 Err((status_code, message)) => Ok(HttpResponse::build(
332 StatusCode::from_u16(status_code).unwrap(),
333 )
334 .body(json!({ "message": message }).to_string())),
335 }
336}
337
338#[cfg_attr(feature = "plugin_utoipa", utoipa::path(
344 context_path = "/api/auth",
345 responses(
346 (status = 200, description = "uses the \"refresh_token\" cookie to give the user a new session", body=AuthTokenResponse),
347 (status = 401, description = "Invalid session.", body = AuthMessageResponse),
348 (status = 401, description = "Invalid token.", body = AuthMessageResponse),
349 ),
350 tag = "Sessions",
351))]
352#[post("/refresh")]
353#[allow(clippy::future_not_send)] async fn refresh(db: Data<Database>, req: HttpRequest) -> Result<HttpResponse, AWError> {
355 let refresh_token = req
356 .cookie(COOKIE_NAME)
357 .map(|cookie| String::from(cookie.value()));
358
359 let result = web::block(move || {
360 controller::refresh(&db, refresh_token.as_ref().map(std::convert::AsRef::as_ref))
361 })
362 .await?;
363
364 match result {
365 Ok((access_token, refresh_token)) => Ok(HttpResponse::build(StatusCode::OK)
366 .cookie(
367 Cookie::build(COOKIE_NAME, refresh_token)
368 .secure(true)
369 .http_only(true)
370 .same_site(SameSite::Strict)
371 .path("/")
372 .finish(),
373 )
374 .body(json!({ "access_token": access_token }).to_string())),
375 Err((status_code, message)) => Ok(HttpResponse::build(
376 StatusCode::from_u16(status_code).unwrap(),
377 )
378 .body(json!({ "message": message }).to_string())),
379 }
380}
381
382#[cfg_attr(feature = "plugin_utoipa", utoipa::path(
390 context_path = "/api/auth",
391 request_body(content = RegisterInput, content_type = "application/json"),
392 responses(
393 (status = 200, description = "Success, sends an email to the user with a link that will let them activate their account", body=AuthMessageResponse),
394 (status = 400, description = "Already registered.", body = AuthMessageResponse),
395 ),
396 tag = "Users",
397))]
398#[post("/register")]
399async fn register(
400 db: Data<Database>,
401 Json(item): Json<RegisterInput>,
402 mailer: Data<Mailer>,
403) -> Result<HttpResponse, AWError> {
404 let result = controller::register(&db, &item, &mailer);
405
406 match result {
407 Ok(()) => Ok(HttpResponse::build(StatusCode::OK)
408 .body("{ \"message\": \"Registered! Check your email to activate your account.\" }")),
409 Err((status_code, message)) => Ok(HttpResponse::build(
410 StatusCode::from_u16(status_code).unwrap(),
411 )
412 .body(json!({ "message": message }).to_string())),
413 }
414}
415
416#[cfg_attr(feature = "plugin_utoipa", utoipa::path(
420 context_path = "/api/auth",
421 params(ActivationInput),
422 responses(
423 (status = 200, description = "Success, account associated with activation_token is activated", body=AuthMessageResponse),
424 (status = 200, description = "Already activated.", body = AuthMessageResponse),
425 (status = 400, description = "Invalid token.", body = AuthMessageResponse),
426 (status = 401, description = "Invalid token", body = AuthMessageResponse),
427 (status = 500, description = "Could not activate user. ", body = AuthMessageResponse),
428 ),
429 tag = "Users",
430))]
431#[get("/activate")]
432async fn activate(
433 db: Data<Database>,
434 Query(item): Query<ActivationInput>,
435 mailer: Data<Mailer>,
436) -> Result<HttpResponse, AWError> {
437 let result = controller::activate(&db, &item, &mailer);
438
439 match result {
440 Ok(()) => Ok(HttpResponse::build(StatusCode::OK).body("{ \"message\": \"Activated!\" }")),
441 Err((status_code, message)) => Ok(HttpResponse::build(
442 StatusCode::from_u16(status_code).unwrap(),
443 )
444 .body(json!({ "message": message }).to_string())),
445 }
446}
447
448#[cfg_attr(feature = "plugin_utoipa", utoipa::path(
458 context_path = "/api/auth",
459 request_body(content = ForgotInput, content_type = "application/json"),
460 responses(
461 (status = 200, description = "Success, password reset email is sent to users email", body=AuthMessageResponse),
462 ),
463 tag = "Users",
464))]
465#[post("/forgot")]
466async fn forgot_password(
467 db: Data<Database>,
468 Json(item): Json<ForgotInput>,
469 mailer: Data<Mailer>,
470) -> Result<HttpResponse, AWError> {
471 let result = controller::forgot_password(&db, &item, &mailer);
472
473 match result {
474 Ok(()) => Ok(HttpResponse::build(StatusCode::OK)
475 .body("{ \"message\": \"Please check your email.\" }")),
476 Err((status_code, message)) => Ok(HttpResponse::build(
477 StatusCode::from_u16(status_code).unwrap(),
478 )
479 .body(json!({ "message": message }).to_string())),
480 }
481}
482
483#[cfg_attr(feature = "plugin_utoipa", utoipa::path(
490 context_path = "/api/auth",
491 request_body(content = ChangeInput, content_type = "application/json"),
492 responses(
493 (status = 200, description = "Success, password changed", body=AuthMessageResponse),
494 (status = 400, description = "Missing password.", body=AuthMessageResponse),
495 (status = 400, description = "The new password must be different.", body=AuthMessageResponse),
496 (status = 400, description = "Account has not been activated.", body=AuthMessageResponse),
497 (status = 400, description = "Invalid credentials.", body=AuthMessageResponse),
498 (status = 500, description = "Could not find user.", body=AuthMessageResponse),
499 (status = 500, description = "Could not update password.", body=AuthMessageResponse),
500 ),
501 tag = "Users",
502 security ( ("JWT" = []))
503))]
504#[post("/change")]
505async fn change_password(
506 db: Data<Database>,
507 Json(item): Json<ChangeInput>,
508 auth: Auth,
509 mailer: Data<Mailer>,
510) -> Result<HttpResponse, AWError> {
511 let result = controller::change_password(&db, &item, &auth, &mailer);
512
513 match result {
514 Ok(()) => Ok(HttpResponse::build(StatusCode::OK)
515 .body(json!({"message": "Password changed."}).to_string())),
516 Err((status_code, message)) => Ok(HttpResponse::build(
517 StatusCode::from_u16(status_code).unwrap(),
518 )
519 .body(json!({ "message": message }).to_string())),
520 }
521}
522
523#[cfg_attr(feature = "plugin_utoipa", utoipa::path(
527 context_path = "/api/auth",
528 responses(
529 (status = 200, description = "Success, API is running"),
530 ),
531 tag = "Users",
532 security ( ("JWT" = []))
533))]
534#[post("/check")]
535async fn check(auth: Auth) -> HttpResponse {
536 controller::check(&auth);
537 HttpResponse::Ok().finish()
538}
539
540#[cfg_attr(feature = "plugin_utoipa", utoipa::path(
545 context_path = "/api/auth",
546 request_body(content = ResetInput, content_type = "application/json"),
547 responses(
548 (status = 200, description = "Password changed.", body=AuthMessageResponse),
549 (status = 400, description = "Invalid token.", body=AuthMessageResponse),
550 (status = 400, description = "Account has not been activated.", body=AuthMessageResponse),
551 (status = 400, description = "The new password must be different.", body=AuthMessageResponse),
552 (status = 401, description = "Invalid token.", body=AuthMessageResponse),
553 (status = 500, description = "Could not update password.", body=AuthMessageResponse),
554 ),
555 tag = "Users",
556))]
557#[post("/reset")]
558async fn reset_password(
559 db: Data<Database>,
560 Json(item): Json<ResetInput>,
561 mailer: Data<Mailer>,
562) -> Result<HttpResponse, AWError> {
563 let result = controller::reset_password(&db, &item, &mailer);
564
565 match result {
566 Ok(()) => Ok(HttpResponse::build(StatusCode::OK)
567 .body(json!({"message": "Password reset"}).to_string())),
568 Err((status_code, message)) => Ok(HttpResponse::build(
569 StatusCode::from_u16(status_code).unwrap(),
570 )
571 .body(json!({ "message": message }).to_string())),
572 }
573}
574
575#[must_use]
577pub fn endpoints(scope: actix_web::Scope) -> actix_web::Scope {
578 let mut scope = scope
579 .service(sessions)
580 .service(destroy_session)
581 .service(destroy_sessions)
582 .service(login)
583 .service(logout)
584 .service(check)
585 .service(refresh)
586 .service(register)
587 .service(activate)
588 .service(forgot_password)
589 .service(change_password)
590 .service(reset_password);
591
592 #[cfg(feature = "plugin_auth-oidc")]
593 {
594 scope = scope.service(oidc_login_redirect);
595 scope = scope.service(oidc_login);
596 }
597
598 scope
599}
600
601#[cfg(feature = "plugin_utoipa")]
603#[derive(OpenApi)]
604#[openapi(
605 paths(sessions, destroy_session, destroy_sessions, login, logout, refresh, register, activate, forgot_password, change_password, check, reset_password),
606 components(
607 schemas(UserSessionResponse, UserSessionJson, AuthMessageResponse, AuthTokenResponse, LoginInput, RegisterInput, ForgotInput, ChangeInput, ResetInput)
608 ),
609 tags(
610 (name = "Auth", description = "users and user_sessions management endpoints"),
611 (name = "Sessions", description = "Endpoints for user_sessions management"),
612 (name = "Users", description = "Endpoints for useres management"),
613 ),
614 modifiers(&JwtSecurityAddon)
615)]
616pub struct ApiDoc;