1use crate::domain::authorization::AuthorizedResponse;
6use actix_web::{
7 HttpResponse, HttpResponseBuilder, error,
8 http::{StatusCode, header::ContentType},
9};
10use backtrace::Backtrace;
11use derive_more::Display;
12use dpop_verifier::error::DpopError;
13use headless_lms_models::{ModelError, ModelErrorType};
14use headless_lms_utils::error::{
15 backend_error::BackendError, backtrace_formatter::format_backtrace, util_error::UtilError,
16};
17use serde::{Deserialize, Serialize};
18use tracing_error::SpanTrace;
19#[cfg(feature = "ts_rs")]
20use ts_rs::TS;
21use uuid::Uuid;
22
23pub type ControllerResult<T, E = ControllerError> = std::result::Result<AuthorizedResponse<T>, E>;
30
31#[derive(Debug, Display, Serialize, Deserialize)]
33pub enum ControllerErrorType {
34 #[display("Internal server error")]
36 InternalServerError,
37
38 #[display("Bad request")]
40 BadRequest,
41
42 #[display("Bad request")]
44 BadRequestWithData(ErrorData),
45
46 #[display("Not found")]
48 NotFound,
49
50 #[display("Unauthorized")]
52 Unauthorized,
53
54 #[display("Forbidden")]
56 Forbidden,
57
58 #[display("OAuthError")]
60 OAuthError(Box<OAuthErrorData>),
61}
62
63pub struct ControllerError {
127 error_type: <ControllerError as BackendError>::ErrorType,
128 message: String,
129 source: Option<anyhow::Error>,
131 span_trace: Box<SpanTrace>,
133 backtrace: Box<Backtrace>,
135}
136
137impl std::fmt::Debug for ControllerError {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.debug_struct("ControllerError")
141 .field("error_type", &self.error_type)
142 .field("message", &self.message)
143 .field("source", &self.source)
144 .finish()?;
145
146 f.write_str("\n\nOperating system thread stack backtrace:\n")?;
147 format_backtrace(&self.backtrace, f)?;
148
149 f.write_str("\n\nTokio tracing span trace:\n")?;
150 f.write_fmt(format_args!("{}\n", &self.span_trace))?;
151
152 Ok(())
153 }
154}
155
156impl std::error::Error for ControllerError {
157 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
158 self.source.as_ref().and_then(|o| o.source())
159 }
160
161 fn cause(&self) -> Option<&dyn std::error::Error> {
162 self.source()
163 }
164}
165
166impl std::fmt::Display for ControllerError {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 write!(
169 f,
170 "ControllerError {:?} {:?}",
171 self.error_type, self.message
172 )
173 }
174}
175
176impl BackendError for ControllerError {
177 type ErrorType = ControllerErrorType;
178
179 fn new<M: Into<String>, S: Into<Option<anyhow::Error>>>(
180 error_type: Self::ErrorType,
181 message: M,
182 source_error: S,
183 ) -> Self {
184 Self::new_with_traces(
185 error_type,
186 message,
187 source_error,
188 Backtrace::new(),
189 SpanTrace::capture(),
190 )
191 }
192
193 fn backtrace(&self) -> Option<&Backtrace> {
194 Some(&self.backtrace)
195 }
196
197 fn error_type(&self) -> &Self::ErrorType {
198 &self.error_type
199 }
200
201 fn message(&self) -> &str {
202 &self.message
203 }
204
205 fn span_trace(&self) -> &SpanTrace {
206 &self.span_trace
207 }
208
209 fn new_with_traces<M: Into<String>, S: Into<Option<anyhow::Error>>>(
210 error_type: Self::ErrorType,
211 message: M,
212 source_error: S,
213 backtrace: Backtrace,
214 span_trace: SpanTrace,
215 ) -> Self {
216 Self {
217 error_type,
218 message: message.into(),
219 source: source_error.into(),
220 span_trace: Box::new(span_trace),
221 backtrace: Box::new(backtrace),
222 }
223 }
224}
225
226#[derive(Debug, Serialize, Deserialize, Clone)]
227#[cfg_attr(feature = "ts_rs", derive(TS))]
228#[serde(rename_all = "snake_case")]
229pub enum ErrorData {
230 BlockId(Uuid),
231}
232
233#[derive(Debug, Serialize, Deserialize)]
235#[cfg_attr(feature = "ts_rs", derive(TS))]
236pub struct ErrorResponse {
237 pub title: String,
238 pub message: String,
239 pub source: Option<String>,
240 pub data: Option<ErrorData>,
241}
242
243impl error::ResponseError for ControllerError {
244 fn error_response(&self) -> HttpResponse {
245 if let ControllerErrorType::InternalServerError = &self.error_type {
246 use std::fmt::Write as _;
247 let mut err_string = String::new();
248 let mut source = Some(self as &dyn std::error::Error);
249 while let Some(err) = source {
250 let _ = write!(err_string, "{}\n ", err);
251 source = err.source();
252 }
253 error!("Internal server error: {}", err_string);
254 }
255 if let ControllerErrorType::OAuthError(data) = &self.error_type {
256 if let Some(uri) = &data.redirect_uri {
257 if let Ok(mut url) = url::Url::parse(uri) {
258 {
259 let mut qp = url.query_pairs_mut();
260 qp.append_pair("error", &data.error);
261 qp.append_pair("error_description", &data.error_description);
262 if let Some(state) = &data.state {
263 qp.append_pair("state", state);
264 }
265 }
266 let loc = url.to_string();
267 return HttpResponse::Found()
268 .append_header(("Location", loc))
269 .finish();
270 }
271 }
272
273 let status = match data.error.as_str() {
274 "invalid_client" => StatusCode::UNAUTHORIZED, "invalid_token" => StatusCode::UNAUTHORIZED, "invalid_dpop_proof" => StatusCode::UNAUTHORIZED, "use_dpop_nonce" => StatusCode::UNAUTHORIZED, "insufficient_scope" => StatusCode::FORBIDDEN, _ => StatusCode::BAD_REQUEST,
280 };
281
282 let mut res = HttpResponse::build(status);
283 fn escape_auth_param(s: &str) -> String {
285 s.replace('\\', "\\\\").replace('"', "\\\"")
286 }
287
288 match data.error.as_str() {
289 "invalid_client" | "invalid_token" | "insufficient_scope" | "invalid_request" => {
291 let err = escape_auth_param(&data.error);
292 let desc = escape_auth_param(&data.error_description);
293 let hdr = format!(r#"Bearer error="{}", error_description="{}""#, err, desc);
294 res.append_header(("WWW-Authenticate", hdr));
295 }
296
297 "invalid_dpop_proof" => {
299 let err = escape_auth_param(&data.error);
300 let desc = escape_auth_param(&data.error_description);
301 let hdr = format!(r#"DPoP error="{}", error_description="{}""#, err, desc);
302 res.append_header(("WWW-Authenticate", hdr));
303 }
304
305 "use_dpop_nonce" => {
306 let err = escape_auth_param(&data.error);
307 let desc = escape_auth_param(&data.error_description);
308 let hdr = format!(r#"DPoP error="{}", error_description="{}""#, err, desc);
309 res.append_header(("WWW-Authenticate", hdr));
310
311 if let Some(nonce) = &data.nonce {
313 res.append_header(("DPoP-Nonce", nonce.clone()));
314 }
315 }
316
317 _ => {}
318 }
319
320 res.append_header(("Cache-Control", "no-store"))
322 .append_header(("Pragma", "no-cache"));
323
324 return res.json(serde_json::json!({
325 "error": data.error,
326 "error_description": data.error_description
327 }));
328 }
329
330 let status = self.status_code();
331
332 let error_data = match &self.error_type {
333 ControllerErrorType::BadRequestWithData(data) => Some(data.clone()),
334 _ => None,
335 };
336
337 let source_message = self.source.as_ref().map(|anyhow_err| {
338 if let Some(controller_err) = anyhow_err.downcast_ref::<ControllerError>() {
339 controller_err.message.clone()
340 } else {
341 anyhow_err.to_string()
342 }
343 });
344
345 let error_response = ErrorResponse {
346 title: status
347 .canonical_reason()
348 .map(str::to_string)
349 .unwrap_or_else(|| status.to_string()),
350 message: self.message.clone(),
351 source: source_message,
352 data: error_data,
353 };
354
355 HttpResponseBuilder::new(status)
356 .append_header(ContentType::json())
357 .body(
358 serde_json::to_string(&error_response).unwrap_or_else(|_| {
359 r#"{"title":"Internal Server Error","message":"Error occurred while formatting error message."}"#.to_string()
360 }),
361 )
362 }
363
364 fn status_code(&self) -> StatusCode {
365 match self.error_type {
366 ControllerErrorType::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
367 ControllerErrorType::BadRequest => StatusCode::BAD_REQUEST,
368 ControllerErrorType::BadRequestWithData(_) => StatusCode::BAD_REQUEST,
369 ControllerErrorType::NotFound => StatusCode::NOT_FOUND,
370 ControllerErrorType::Unauthorized => StatusCode::UNAUTHORIZED,
371 ControllerErrorType::Forbidden => StatusCode::FORBIDDEN,
372 ControllerErrorType::OAuthError(_) => StatusCode::OK,
373 }
374 }
375}
376
377#[derive(Debug, Serialize, Deserialize, Clone)]
378#[serde(rename_all = "snake_case")]
379pub struct OAuthErrorData {
380 pub error: String,
381 pub error_description: String,
382 pub redirect_uri: Option<String>,
383 pub state: Option<String>,
384 pub nonce: Option<String>,
385}
386
387pub enum OAuthErrorCode {
388 InvalidGrant,
389 InvalidRequest,
390 InvalidClient,
391 InvalidToken,
392 InsufficientScope,
393 UnsupportedGrantType,
394 UnsupportedResponseType,
395 ServerError,
396 InvalidDpopProof,
397 UseDpopNonce,
398}
399
400impl OAuthErrorCode {
401 pub fn as_str(&self) -> &'static str {
402 match self {
403 Self::InvalidGrant => "invalid_grant",
404 Self::InvalidRequest => "invalid_request",
405 Self::InvalidClient => "invalid_client",
406 Self::InvalidToken => "invalid_token",
407 Self::InsufficientScope => "insufficient_scope",
408 Self::UnsupportedGrantType => "unsupported_grant_type",
409 Self::UnsupportedResponseType => "unsupported_response_type",
410 Self::ServerError => "server_error",
411 Self::InvalidDpopProof => "invalid_dpop_proof",
412 Self::UseDpopNonce => "use_dpop_nonce",
413 }
414 }
415}
416
417impl From<anyhow::Error> for ControllerError {
418 fn from(err: anyhow::Error) -> ControllerError {
419 if let Some(sqlx::Error::RowNotFound) = err.downcast_ref::<sqlx::Error>() {
420 return Self::new(ControllerErrorType::NotFound, err.to_string(), Some(err));
421 }
422
423 Self::new(
424 ControllerErrorType::InternalServerError,
425 err.to_string(),
426 Some(err),
427 )
428 }
429}
430
431impl From<uuid::Error> for ControllerError {
432 fn from(err: uuid::Error) -> ControllerError {
433 Self::new(
434 ControllerErrorType::BadRequest,
435 err.to_string(),
436 Some(err.into()),
437 )
438 }
439}
440
441impl From<sqlx::Error> for ControllerError {
442 fn from(err: sqlx::Error) -> ControllerError {
443 Self::new(
444 ControllerErrorType::InternalServerError,
445 err.to_string(),
446 Some(err.into()),
447 )
448 }
449}
450
451impl From<git2::Error> for ControllerError {
452 fn from(err: git2::Error) -> ControllerError {
453 Self::new(
454 ControllerErrorType::InternalServerError,
455 err.to_string(),
456 Some(err.into()),
457 )
458 }
459}
460
461impl From<actix_web::Error> for ControllerError {
462 fn from(err: actix_web::Error) -> Self {
463 Self::new(
464 ControllerErrorType::InternalServerError,
465 err.to_string(),
466 None,
467 )
468 }
469}
470
471impl From<actix_multipart::MultipartError> for ControllerError {
472 fn from(err: actix_multipart::MultipartError) -> Self {
473 Self::new(
474 ControllerErrorType::InternalServerError,
475 err.to_string(),
476 None,
477 )
478 }
479}
480
481impl From<jsonwebtoken::errors::Error> for ControllerError {
482 fn from(err: jsonwebtoken::errors::Error) -> Self {
483 Self::new(
484 ControllerErrorType::InternalServerError,
485 err.to_string(),
486 None,
487 )
488 }
489}
490
491impl From<ModelError> for ControllerError {
492 fn from(err: ModelError) -> Self {
493 let backtrace: Backtrace =
494 match headless_lms_utils::error::backend_error::BackendError::backtrace(&err) {
495 Some(backtrace) => backtrace.clone(),
496 _ => Backtrace::new(),
497 };
498 let span_trace = err.span_trace().clone();
499 match err.error_type() {
500 ModelErrorType::RecordNotFound => Self::new_with_traces(
501 ControllerErrorType::NotFound,
502 err.to_string(),
503 Some(err.into()),
504 backtrace,
505 span_trace,
506 ),
507 ModelErrorType::NotFound => Self::new_with_traces(
508 ControllerErrorType::NotFound,
509 err.to_string(),
510 Some(err.into()),
511 backtrace,
512 span_trace,
513 ),
514 ModelErrorType::PreconditionFailed => Self::new_with_traces(
515 ControllerErrorType::BadRequest,
516 err.message().to_string(),
517 Some(err.into()),
518 backtrace,
519 span_trace,
520 ),
521 ModelErrorType::PreconditionFailedWithCMSAnchorBlockId { description, id } => {
522 Self::new_with_traces(
523 ControllerErrorType::BadRequestWithData(ErrorData::BlockId(*id)),
524 description.to_string(),
525 Some(err.into()),
526 backtrace,
527 span_trace,
528 )
529 }
530 ModelErrorType::DatabaseConstraint { description, .. } => Self::new_with_traces(
531 ControllerErrorType::BadRequest,
532 description.to_string(),
533 Some(err.into()),
534 backtrace,
535 span_trace,
536 ),
537 ModelErrorType::InvalidRequest => Self::new_with_traces(
538 ControllerErrorType::BadRequest,
539 err.message().to_string(),
540 Some(err.into()),
541 backtrace,
542 span_trace,
543 ),
544 _ => Self::new_with_traces(
545 ControllerErrorType::InternalServerError,
546 err.to_string(),
547 Some(err.into()),
548 backtrace,
549 span_trace,
550 ),
551 }
552 }
553}
554
555impl From<UtilError> for ControllerError {
556 fn from(err: UtilError) -> Self {
557 let backtrace: Backtrace =
558 match headless_lms_utils::error::backend_error::BackendError::backtrace(&err) {
559 Some(backtrace) => backtrace.clone(),
560 _ => Backtrace::new(),
561 };
562 let span_trace = err.span_trace().clone();
563 Self::new_with_traces(
564 ControllerErrorType::InternalServerError,
565 err.to_string(),
566 Some(err.into()),
567 backtrace,
568 span_trace,
569 )
570 }
571}
572
573impl From<serde_json::Error> for ControllerError {
574 fn from(err: serde_json::Error) -> Self {
575 Self::new(
576 ControllerErrorType::InternalServerError,
577 err.to_string(),
578 Some(err.into()),
579 )
580 }
581}
582
583impl From<base64::DecodeError> for ControllerError {
584 fn from(err: base64::DecodeError) -> Self {
585 Self::new(
586 ControllerErrorType::InternalServerError,
587 err.to_string(),
588 Some(err.into()),
589 )
590 }
591}
592
593impl From<std::string::FromUtf8Error> for ControllerError {
594 fn from(err: std::string::FromUtf8Error) -> Self {
595 Self::new(
596 ControllerErrorType::InternalServerError,
597 err.to_string(),
598 Some(err.into()),
599 )
600 }
601}
602
603impl From<pkcs8::spki::Error> for ControllerError {
604 fn from(err: pkcs8::spki::Error) -> Self {
605 Self::new(
606 ControllerErrorType::InternalServerError,
607 err.to_string(),
608 Some(err.into()),
609 )
610 }
611}
612
613impl From<dpop_verifier::error::DpopError> for ControllerError {
614 fn from(err: DpopError) -> Self {
615 let oauth_error = match &err {
616 DpopError::MultipleDpopHeaders
617 | DpopError::InvalidDpopHeader
618 | DpopError::MissingDpopHeader
619 | DpopError::MalformedJws
620 | DpopError::InvalidAlg(_)
621 | DpopError::UnsupportedAlg(_)
622 | DpopError::InvalidSignature
623 | DpopError::BadJwk(_)
624 | DpopError::MissingClaim(_)
625 | DpopError::InvalidMethod
626 | DpopError::HtmMismatch
627 | DpopError::MalformedHtu
628 | DpopError::HtuMismatch
629 | DpopError::AthMalformed
630 | DpopError::MissingAth
631 | DpopError::AthMismatch
632 | DpopError::FutureSkew
633 | DpopError::Stale
634 | DpopError::Replay
635 | DpopError::JtiTooLong
636 | DpopError::NonceMismatch
637 | DpopError::NonceStale
638 | DpopError::InvalidHmacConfig
639 | DpopError::MissingNonce => OAuthErrorData {
640 error: OAuthErrorCode::InvalidDpopProof.as_str().into(),
641 error_description: err.to_string(),
642 redirect_uri: None,
643 state: None,
644 nonce: None,
645 },
646
647 DpopError::Store(e) => OAuthErrorData {
648 error: OAuthErrorCode::ServerError.as_str().into(),
649 error_description: format!("DPoP storage error: {e}"),
650 redirect_uri: None,
651 state: None,
652 nonce: None,
653 },
654
655 DpopError::UseDpopNonce { nonce } => OAuthErrorData {
656 error: OAuthErrorCode::UseDpopNonce.as_str().into(), error_description: "Server requires DPoP nonce".into(),
658 redirect_uri: None,
659 state: None,
660 nonce: Some(nonce.clone()),
661 },
662 };
663
664 ControllerError::new(
665 ControllerErrorType::OAuthError(Box::new(oauth_error)),
666 err.to_string(),
667 Some(err.into()),
668 )
669 }
670}
671
672#[derive(Debug, thiserror::Error)]
673pub enum PkceFlowError {
674 #[error("{0}")]
676 InvalidRequest(&'static str),
677
678 #[error("{0}")]
680 InvalidGrant(&'static str),
681
682 #[error("{0}")]
684 ServerError(&'static str),
685}
686
687impl From<PkceFlowError> for ControllerError {
688 fn from(err: PkceFlowError) -> Self {
689 let data = match &err {
690 PkceFlowError::InvalidRequest(msg) => OAuthErrorData {
691 error: OAuthErrorCode::InvalidRequest.as_str().into(),
692 error_description: (*msg).into(),
693 redirect_uri: None,
694 state: None,
695 nonce: None,
696 },
697 PkceFlowError::InvalidGrant(msg) => OAuthErrorData {
698 error: OAuthErrorCode::InvalidGrant.as_str().into(),
699 error_description: (*msg).into(),
700 redirect_uri: None,
701 state: None,
702 nonce: None,
703 },
704 PkceFlowError::ServerError(msg) => OAuthErrorData {
705 error: OAuthErrorCode::ServerError.as_str().into(),
706 error_description: (*msg).into(),
707 redirect_uri: None,
708 state: None,
709 nonce: None,
710 },
711 };
712
713 ControllerError::new(
714 ControllerErrorType::OAuthError(Box::new(data)),
715 err.to_string(),
716 Some(anyhow::anyhow!(err)),
717 )
718 }
719}
720
721impl From<crate::domain::oauth::pkce::PkceError> for PkceFlowError {
722 fn from(_err: crate::domain::oauth::pkce::PkceError) -> Self {
723 PkceFlowError::InvalidRequest("invalid code_verifier")
725 }
726}
727
728impl From<crate::domain::oauth::pkce::PkceError> for ControllerError {
729 fn from(err: crate::domain::oauth::pkce::PkceError) -> Self {
730 PkceFlowError::from(err).into()
731 }
732}