headless_lms_server/controllers/main_frontend/
time.rs

1/*!
2Handlers for HTTP requests to `/api/v0/main-frontend/time`.
3*/
4
5use crate::prelude::*;
6use chrono::{SecondsFormat, Utc};
7use utoipa::OpenApi;
8
9#[derive(OpenApi)]
10#[openapi(paths(get_current_time))]
11pub(crate) struct MainFrontendTimeApiDoc;
12
13/**
14GET `/api/v0/main-frontend/time` Returns the server's current UTC time as an RFC3339 timestamp string.
15
16Response body example:
17`"2026-02-18T12:34:56.789Z"`
18*/
19#[utoipa::path(
20    get,
21    path = "",
22    operation_id = "getCurrentTime",
23    tag = "time",
24    responses(
25        (status = 200, description = "Current server time", body = String)
26    )
27)]
28pub async fn get_current_time() -> ControllerResult<HttpResponse> {
29    let server_time = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
30    let token = skip_authorize();
31
32    token.authorized_ok(
33        HttpResponse::Ok()
34            .insert_header((
35                "Cache-Control",
36                "no-store, no-cache, must-revalidate, max-age=0",
37            ))
38            .insert_header(("Pragma", "no-cache"))
39            .insert_header(("Expires", "0"))
40            .json(server_time),
41    )
42}
43
44pub fn _add_routes(cfg: &mut ServiceConfig) {
45    cfg.route("", web::get().to(get_current_time));
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use actix_web::{App, test, web};
52
53    #[actix_web::test]
54    async fn returns_non_cacheable_rfc3339_json_string() {
55        let app = test::init_service(App::new().service(web::scope("/api/v0").service(
56            web::scope("/main-frontend").service(web::scope("/time").configure(_add_routes)),
57        )))
58        .await;
59
60        let req = test::TestRequest::with_uri("/api/v0/main-frontend/time").to_request();
61        let resp = test::call_service(&app, req).await;
62
63        assert!(resp.status().is_success());
64        assert_eq!(
65            resp.headers().get("Cache-Control").unwrap(),
66            "no-store, no-cache, must-revalidate, max-age=0"
67        );
68        assert_eq!(resp.headers().get("Pragma").unwrap(), "no-cache");
69        assert_eq!(resp.headers().get("Expires").unwrap(), "0");
70
71        let body: String = test::read_body_json(resp).await;
72        assert!(chrono::DateTime::parse_from_rfc3339(&body).is_ok());
73
74        let parsed = chrono::DateTime::parse_from_rfc3339(&body).unwrap();
75        let server_utc = parsed.with_timezone(&chrono::Utc);
76        let now = chrono::Utc::now();
77        let diff = (server_utc - now).abs();
78        assert!(
79            diff < chrono::Duration::seconds(5),
80            "server time should be close to now"
81        );
82    }
83}