Skip to main content

headless_lms_server/controllers/
mod.rs

1/*!
2Handlers for HTTP requests to `/api/v0`.
3
4This documents all endpoints. Select a module below for a namespace.
5
6*/
7
8// tracing::instrument seems to have issues with this
9#![allow(clippy::suspicious_else_formatting)]
10
11pub mod auth;
12pub mod cms;
13pub mod course_material;
14pub mod errors;
15pub mod exercise_services;
16pub mod files;
17pub mod health;
18pub mod helpers;
19pub mod main_frontend;
20pub mod mock_azure;
21pub mod mock_document_storage;
22pub mod mock_sisu;
23pub mod mock_suotar;
24pub mod other_domain_redirects;
25pub mod study_registry;
26pub mod tmc_server;
27pub mod well_known;
28use crate::domain::error::{ControllerError, ControllerErrorType};
29use crate::domain::rate_limit_middleware_builder::{RateLimit, RateLimitConfig, RateLimitKey};
30use actix_web::{
31    HttpRequest, HttpResponse, ResponseError,
32    web::{self, ServiceConfig},
33};
34use headless_lms_utils::prelude::*;
35use serde::{Deserialize, Serialize};
36use utoipa::ToSchema;
37
38/// Result of a image upload. Tells where the uploaded image can be retrieved from.
39#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
40
41pub struct UploadResult {
42    pub url: String,
43}
44
45/// Add controllers from all the submodules.
46pub fn configure_controllers(
47    cfg: &mut ServiceConfig,
48    app_conf: web::Data<ApplicationConfiguration>,
49) {
50    cfg.service(web::scope("/course-material").configure(course_material::_add_routes))
51        .service(web::scope("/cms").configure(cms::_add_routes))
52        .service(web::scope("/files").configure(files::_add_routes))
53        .service(web::scope("/main-frontend").configure(main_frontend::_add_routes))
54        .service(web::scope("/auth").configure(auth::_add_routes))
55        .service(web::scope("/errors").configure(errors::_add_routes))
56        .service(web::scope("/study-registry").configure(study_registry::_add_routes))
57        .service(web::scope("/exercise-services").configure(exercise_services::_add_routes))
58        .service(
59            web::scope("/other-domain-redirects").configure(other_domain_redirects::_add_routes),
60        )
61        .service(web::scope("/health").configure(health::_add_routes))
62        .service(web::scope("/tmc-server").configure(tmc_server::_add_routes))
63        .service({
64            let [per_grant, per_user] = mcp_rate_limits(app_conf.test_mode);
65            // Registered inside out: authentication runs first and gives the limits their key.
66            web::scope("/mcp")
67                .wrap(per_grant)
68                .wrap(per_user)
69                .wrap(actix_web::middleware::from_fn(
70                    headless_lms_mcp::http::auth::authenticate,
71                ))
72                .configure(headless_lms_mcp::http::configure)
73        })
74        .default_service(web::to(not_found));
75    if app_conf.test_chatbot && app_conf.test_mode {
76        cfg.service(web::scope("/mock-azure").configure(mock_azure::_add_routes))
77            .service(
78                web::scope("/mock-document-storage").configure(mock_document_storage::_add_routes),
79            );
80    }
81    if app_conf.test_sisu && app_conf.test_mode {
82        cfg.service(web::scope("/mock-sisu").configure(mock_sisu::_add_routes));
83    }
84    if app_conf.test_suotar && app_conf.test_mode {
85        cfg.service(mock_suotar::scope());
86    }
87}
88
89/// The MCP server's limits, per grant and per user. A user's grants together get twice what one
90/// grant does, so one runaway agent cannot use up the quota of the user's other agent.
91fn mcp_rate_limits(test_mode: bool) -> [RateLimit; 2] {
92    let multiplier = if test_mode { 100 } else { 1 };
93    let limit = |per_minute: u64, per_hour: u64, key| {
94        RateLimit::new(RateLimitConfig {
95            per_minute: Some(per_minute * multiplier),
96            per_hour: Some(per_hour * multiplier),
97            ..Default::default()
98        })
99        .keyed_by(key)
100    };
101    [
102        limit(120, 3000, RateLimitKey::Grant),
103        limit(240, 6000, RateLimitKey::User),
104    ]
105}
106
107async fn not_found(req: HttpRequest) -> HttpResponse {
108    ControllerError::new(
109        ControllerErrorType::NotFound,
110        format!("No handler found for route '{}'", req.path()),
111        None,
112    )
113    .error_response()
114}