rustream/routes/
auth.rs

1use std::sync::Arc;
2
3use actix_web::{HttpRequest, HttpResponse, web};
4use actix_web::cookie::{Cookie, SameSite};
5use actix_web::cookie::time::{Duration, OffsetDateTime};
6use actix_web::http::StatusCode;
7use fernet::Fernet;
8use minijinja;
9use serde::Serialize;
10
11use crate::{constant, squire};
12
13/// Struct for representing a JSON Response with a redirect URL.
14#[derive(Serialize)]
15struct RedirectResponse {
16    redirect_url: String,
17}
18
19/// Struct for representing detailed errors in JSON format.
20#[derive(Serialize)]
21pub struct DetailError {
22    pub detail: String,
23}
24
25/// Handles the login endpoint, verifying credentials and creating session tokens.
26///
27/// # Arguments
28///
29/// * `request` - A reference to the Actix web `HttpRequest` object.
30/// * `config` - Configuration data for the application.
31/// * `fernet` - Fernet object to encrypt the auth payload that will be set as `session_token` cookie.
32/// * `session` - Session struct that holds the `session_mapping` and `session_tracker` to handle sessions.
33///
34/// # Returns
35///
36/// * `200` - HttpResponse with a `session_token` and redirect URL to the `/home` entrypoint.
37/// * `401` - HttpResponse with an error message for failed authentication.
38#[post("/login")]
39pub async fn login(request: HttpRequest,
40                   config: web::Data<Arc<squire::settings::Config>>,
41                   fernet: web::Data<Arc<Fernet>>,
42                   session: web::Data<Arc<constant::Session>>) -> HttpResponse {
43    let verified = squire::authenticator::verify_login(&request, &config, &session);
44    if let Err(err) = verified {
45        let err_message = err.to_string();
46        log::warn!("Error response::{}", err_message);
47        return HttpResponse::Unauthorized().json(DetailError {
48            detail: err_message
49        });
50    }
51
52    let mapped = verified.unwrap();
53    let (_host, _last_accessed) = squire::custom::log_connection(&request, &session);
54
55    let payload = serde_json::to_string(&mapped).unwrap();
56    let encrypted_payload = fernet.encrypt(payload.as_bytes());
57
58    let cookie_duration = Duration::seconds(config.session_duration);
59    let expiration = OffsetDateTime::now_utc() + cookie_duration;
60    let base_cookie = Cookie::build("session_token", encrypted_payload)
61        .http_only(true)
62        .same_site(SameSite::Strict)
63        .max_age(cookie_duration)
64        .expires(expiration);
65
66    let cookie = if config.secure_session {
67        log::info!("Marking 'session_token' cookie as secure!!");
68        base_cookie.secure(true).finish()
69    } else {
70        base_cookie.finish()
71    };
72    log::info!("Session for '{}' will be valid until {}", mapped.get("username").unwrap(), expiration);
73
74    let mut response = HttpResponse::Ok().json(RedirectResponse {
75        redirect_url: "/home".to_string(),
76    });
77    response.add_cookie(&cookie).unwrap();
78    response
79}
80
81/// Handles the logout endpoint, logging out the user and rendering the appropriate HTML page.
82///
83/// # Arguments
84///
85/// * `request` - A reference to the Actix web `HttpRequest` object.
86/// * `fernet` - Fernet object to encrypt the auth payload that will be set as `session_token` cookie.
87/// * `session` - Session struct that holds the `session_mapping` and `session_tracker` to handle sessions.
88/// * `metadata` - Struct containing metadata of the application.
89/// * `config` - Configuration data for the application.
90/// * `template` - Configuration container for the loaded templates.
91///
92/// # Returns
93///
94/// Returns an `HTTPResponse` with the cookie for `session_token` reset if available.
95#[get("/logout")]
96pub async fn logout(request: HttpRequest,
97                    fernet: web::Data<Arc<Fernet>>,
98                    session: web::Data<Arc<constant::Session>>,
99                    metadata: web::Data<Arc<constant::MetaData>>,
100                    config: web::Data<Arc<squire::settings::Config>>,
101                    template: web::Data<Arc<minijinja::Environment<'static>>>) -> HttpResponse {
102    let host = request.connection_info().host().to_owned();
103    let logout_template = template.get_template("logout").unwrap();
104    let mut response = HttpResponse::build(StatusCode::OK);
105    response.content_type("text/html; charset=utf-8");
106
107    let rendered;
108    let auth_response = squire::authenticator::verify_token(&request, &config, &fernet, &session);
109    log::debug!("Session Validation Response: {}", auth_response.detail);
110
111    if auth_response.username != "NA" {
112        log::info!("{} from {} attempted to log out", auth_response.username, host)
113    }
114
115    if auth_response.ok {
116        let mut tracker = session.tracker.lock().unwrap();
117        if tracker.get(&host).is_some() {
118            tracker.remove(&host);
119        } else {
120            log::warn!("Session information for {} was not stored or no file was rendered", host);
121        }
122        rendered = logout_template.render(minijinja::context!(
123            version => metadata.pkg_version,
124            detail => "You have been logged out successfully."
125        )).unwrap();
126
127        let mut cookie = Cookie::new("session_token", "");
128        cookie.set_same_site(SameSite::Strict);
129        cookie.make_removal();
130        response.cookie(cookie);
131    } else {
132        log::debug!("No stored session found for {}", host);
133        rendered = logout_template.render(minijinja::context!(
134                version => metadata.pkg_version,
135                detail => "You are not logged in. Please click the button below to proceed.",
136                show_login => true
137            )).unwrap();
138    }
139    // response.finish() is not required since setting the body will close the response
140    response.body(rendered)
141}
142
143/// Handles the home endpoint, rendering the listing page for authenticated users.
144///
145/// # Arguments
146///
147/// * `request` - A reference to the Actix web `HttpRequest` object.
148/// * `fernet` - Fernet object to encrypt the auth payload that will be set as `session_token` cookie.
149/// * `session` - Session struct that holds the `session_mapping` and `session_tracker` to handle sessions.
150/// * `metadata` - Struct containing metadata of the application.
151/// * `config` - Configuration data for the application.
152/// * `template` - Configuration container for the loaded templates.
153///
154/// # Returns
155///
156/// * `200` - Returns an `HTTPResponse` with the home/listing page if session token is valid.
157/// * `401` - HttpResponse with an error message for failed authentication.
158#[get("/home")]
159pub async fn home(request: HttpRequest,
160                  fernet: web::Data<Arc<Fernet>>,
161                  session: web::Data<Arc<constant::Session>>,
162                  metadata: web::Data<Arc<constant::MetaData>>,
163                  config: web::Data<Arc<squire::settings::Config>>,
164                  template: web::Data<Arc<minijinja::Environment<'static>>>) -> HttpResponse {
165    let auth_response = squire::authenticator::verify_token(&request, &config, &fernet, &session);
166    if !auth_response.ok {
167        return failed_auth(auth_response, &config);
168    }
169    let (_host, _last_accessed) = squire::custom::log_connection(&request, &session);
170    log::debug!("{}", auth_response.detail);
171
172    let listing_page = squire::content::get_all_stream_content(&config, &auth_response);
173    let listing = template.get_template("listing").unwrap();
174
175    HttpResponse::build(StatusCode::OK)
176        .content_type("text/html; charset=utf-8")
177        .body(
178            listing.render(minijinja::context!(
179                version => metadata.pkg_version,
180                files => listing_page.files,
181                user => auth_response.username,
182                secure_index => constant::SECURE_INDEX,
183                directories => listing_page.directories,
184                secured_directories => listing_page.secured_directories
185            )).unwrap()
186        )
187}
188
189/// Handles the error endpoint, rendering the appropriate HTML page based on session issues.
190///
191/// # Arguments
192///
193/// * `request` - A reference to the Actix web `HttpRequest` object.
194/// * `metadata` - Struct containing metadata of the application.
195/// * `template` - Configuration container for the loaded templates.
196///
197/// # Returns
198///
199/// HttpResponse with either a session expiry or unauthorized message.
200#[get("/error")]
201pub async fn error(request: HttpRequest,
202                   metadata: web::Data<Arc<constant::MetaData>>,
203                   template: web::Data<Arc<minijinja::Environment<'static>>>) -> HttpResponse {
204    if let Some(detail) = request.cookie("detail") {
205        log::info!("Error response for /error: {}", detail.value());
206        let session = template.get_template("session").unwrap();
207        return HttpResponse::build(StatusCode::UNAUTHORIZED)
208            .content_type("text/html; charset=utf-8")
209            .body(session.render(minijinja::context!(
210                version => metadata.pkg_version,
211                reason => detail.value()
212            )).unwrap());
213    }
214
215    log::info!("Sending unauthorized response for /error");
216    let error = template.get_template("error").unwrap();
217    HttpResponse::build(StatusCode::UNAUTHORIZED)
218        .content_type("text/html; charset=utf-8")
219        .body(error.render(minijinja::context!(
220            version => metadata.pkg_version,
221            title => "LOGIN FAILED",
222            description => "USER ERROR - REPLACE USER",
223            help => r"Forgot Password?\n\nRelax and try to remember your password.",
224            button_text => "LOGIN", button_link => "/",
225            block_navigation => true
226        )).unwrap())
227}
228
229/// Constructs an `HttpResponse` for failed `session_token` verification.
230///
231/// # Arguments
232///
233/// * `auth_response` - The authentication response containing details of the failure.
234/// * `config` - Configuration data for the application.
235///
236/// # Returns
237///
238/// Returns an `HttpResponse` with a redirect, setting a cookie with the failure detail.
239pub fn failed_auth(auth_response: squire::authenticator::AuthToken,
240                   config: &squire::settings::Config) -> HttpResponse {
241    let mut response = HttpResponse::build(StatusCode::FOUND);
242    let detail = auth_response.detail;
243    let age = Duration::new(3, 0);
244    let base_cookie = Cookie::build("detail", detail)
245        .path("/error")
246        .http_only(true)
247        .same_site(SameSite::Strict)
248        .max_age(age);
249    let cookie = if config.secure_session {
250        log::debug!("Marking 'detail' cookie as secure!!");
251        base_cookie.secure(true).finish()
252    } else {
253        base_cookie.finish()
254    };
255    response.cookie(cookie);
256    response.append_header(("Location", "/error"));
257    response.finish()
258}