Skip to content

Commit

Permalink
Implement a profile page to render session info
Browse files Browse the repository at this point in the history
Disable cache in profile page
Don't block navigation on all error pages
  • Loading branch information
dormant-user committed Mar 15, 2024
1 parent 890d4dc commit 43777e8
Show file tree
Hide file tree
Showing 16 changed files with 362 additions and 36 deletions.
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ pub async fn start() -> io::Result<()> {
.service(routes::auth::login)
.service(routes::auth::logout)
.service(routes::auth::home)
.service(routes::basics::profile)
.service(routes::auth::error)
.service(routes::media::track)
.service(routes::media::stream)
Expand Down
7 changes: 4 additions & 3 deletions src/routes/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub async fn login(request: HttpRequest,
}

let mapped = verified.unwrap();
squire::logger::log_connection(&request, &session);
let (_host, _last_accessed) = squire::logger::log_connection(&request, &session);

let payload = serde_json::to_string(&mapped).unwrap();
let encrypted_payload = fernet.encrypt(payload.as_bytes());
Expand Down Expand Up @@ -166,7 +166,7 @@ pub async fn home(request: HttpRequest,
if !auth_response.ok {
return failed_auth(auth_response, &config);
}
squire::logger::log_connection(&request, &session);
let (_host, _last_accessed) = squire::logger::log_connection(&request, &session);
log::debug!("{}", auth_response.detail);

let listing_page = squire::content::get_all_stream_content(&config, &auth_response);
Expand Down Expand Up @@ -221,7 +221,8 @@ pub async fn error(request: HttpRequest,
title => "LOGIN FAILED",
description => "USER ERROR - REPLACE USER",
help => r"Forgot Password?\n\nRelax and try to remember your password.",
button_text => "LOGIN", button_link => "/"
button_text => "LOGIN", button_link => "/",
block_navigation => true
)).unwrap())
}

Expand Down
59 changes: 57 additions & 2 deletions src/routes/basics.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;

use actix_web::{HttpRequest, HttpResponse, web};
use actix_web::http::StatusCode;
use fernet::Fernet;

use crate::{constant, squire};
use crate::{constant, routes, squire};

/// Handles the health endpoint, returning a JSON response indicating the server is healthy.
///
Expand Down Expand Up @@ -35,9 +38,61 @@ pub async fn root(request: HttpRequest,
session: web::Data<Arc<constant::Session>>,
metadata: web::Data<Arc<constant::MetaData>>,
template: web::Data<Arc<minijinja::Environment<'static>>>) -> HttpResponse {
squire::logger::log_connection(&request, &session);
let (_host, _last_accessed) = squire::logger::log_connection(&request, &session);
let index = template.get_template("index").unwrap();
HttpResponse::build(StatusCode::OK)
.content_type("text/html; charset=utf-8")
.body(index.render(minijinja::context!(version => &metadata.pkg_version)).unwrap())
}

/// Handles the profile endpoint, and returns an HTML response.
///
/// # Arguments
///
/// * `request` - A reference to the Actix web `HttpRequest` object.
/// * `fernet` - Fernet object to encrypt the auth payload that will be set as `session_token` cookie.
/// * `session` - Session struct that holds the `session_mapping` and `session_tracker` to handle sessions.
/// * `metadata` - Struct containing metadata of the application.
/// * `config` - Configuration data for the application.
/// * `template` - Configuration container for the loaded templates.
///
/// # Returns
///
/// Returns an `HttpResponse` with the profile page as its body.
#[get("/profile")]
pub async fn profile(request: HttpRequest,
fernet: web::Data<Arc<Fernet>>,
session: web::Data<Arc<constant::Session>>,
metadata: web::Data<Arc<constant::MetaData>>,
config: web::Data<Arc<squire::settings::Config>>,
template: web::Data<Arc<minijinja::Environment<'static>>>) -> HttpResponse {
let auth_response = squire::authenticator::verify_token(&request, &config, &fernet, &session);
if !auth_response.ok {
return routes::auth::failed_auth(auth_response, &config);
}
let (_host, last_accessed) = squire::logger::log_connection(&request, &session);
let index = template.get_template("profile").unwrap();
let mut access_map = HashMap::new();
if !last_accessed.is_empty() {
let filepath = Path::new(&last_accessed);
let extn = filepath.extension().unwrap().to_str().unwrap();
let name = filepath.iter().last().unwrap().to_string_lossy().to_string();
let path = format!("/stream/{}", &last_accessed);
let font = if last_accessed.contains(constant::SECURE_INDEX) {
"fa-solid fa-lock".to_string()
} else {
squire::content::get_file_font(extn)
};
access_map = HashMap::from([
("name", name), ("font", font), ("path", path)
]);
}
HttpResponse::build(StatusCode::OK)
.content_type("text/html; charset=utf-8")
.body(index.render(minijinja::context!(
version => &metadata.pkg_version,
user => &auth_response.username,
time_left => &auth_response.time_left,
file => access_map,
)).unwrap())
}
10 changes: 5 additions & 5 deletions src/routes/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use minijinja;
use serde::Deserialize;
use url::form_urlencoded;


use crate::{constant, routes, squire};

/// Represents the payload structure for deserializing data from the request query parameters.
Expand Down Expand Up @@ -101,7 +102,7 @@ pub async fn track(request: HttpRequest,
&metadata.pkg_version
);
}
squire::logger::log_connection(&request, &session);
let (_host, _last_accessed) = squire::logger::log_connection(&request, &session);
log::debug!("{}", auth_response.detail);
log::debug!("Track requested: {}", &info.file);
let filepath = Path::new(&config.media_source).join(&info.file);
Expand Down Expand Up @@ -163,7 +164,7 @@ pub async fn stream(request: HttpRequest,
if !auth_response.ok {
return routes::auth::failed_auth(auth_response, &config);
}
squire::logger::log_connection(&request, &session);
let (_host, _last_accessed) = squire::logger::log_connection(&request, &session);
log::debug!("{}", auth_response.detail);
let filepath = media_path.to_string();
if !squire::authenticator::verify_secure_index(&PathBuf::from(&filepath), &auth_response.username) {
Expand Down Expand Up @@ -296,15 +297,14 @@ pub async fn streaming_endpoint(request: HttpRequest,
&metadata.pkg_version
);
}
squire::logger::log_connection(&request, &session);
let host = request.connection_info().host().to_owned();
let (host, _last_accessed) = squire::logger::log_connection(&request, &session);
if media_path.exists() {
let file = actix_files::NamedFile::open_async(media_path).await.unwrap();
// Check if the host is making a continued connection streaming the same file
let mut tracker = session.tracker.lock().unwrap();
if tracker.get(&host).unwrap() != &info.file {
log::info!("Streaming {}", info.file);
tracker.insert(request.connection_info().host().to_string(), info.file.to_string());
tracker.insert(host, info.file.to_string());
}
return file.into_response(&request);
}
Expand Down
16 changes: 14 additions & 2 deletions src/squire/authenticator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub struct AuthToken {
pub ok: bool,
pub detail: String,
pub username: String,
pub time_left: i64
}


Expand Down Expand Up @@ -149,6 +150,7 @@ pub fn verify_token(
ok: false,
detail: "Server doesn't recognize your session".to_string(),
username: "NA".to_string(),
time_left: 0
};
}
if let Some(cookie) = request.cookie("session_token") {
Expand All @@ -165,28 +167,38 @@ pub fn verify_token(
ok: false,
detail: "Invalid session token".to_string(),
username,
time_left: 0
};
}
if current_time - timestamp > config.session_duration {
return AuthToken { ok: false, detail: "Session Expired".to_string(), username };
return AuthToken {
ok: false,
detail: "Session Expired".to_string(),
username,
time_left: 0
};
}
let time_left = timestamp + config.session_duration - current_time;
AuthToken {
ok: true,
detail: format!("Session valid for {}s", timestamp + config.session_duration - current_time),
detail: format!("Session valid for {}s", time_left),
username,
time_left
}
} else {
AuthToken {
ok: false,
detail: "Invalid session token".to_string(),
username: "NA".to_string(),
time_left: 0
}
}
} else {
AuthToken {
ok: false,
detail: "Session information not found".to_string(),
username: "NA".to_string(),
time_left: 0
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/squire/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ fn natural_sort_key(regex: &Regex, filename: &str) -> Vec<Result<i32, String>> {
/// # Returns
///
/// A string with the `fa` value based on the file extension.
fn get_file_font(extn: &str) -> String {
pub fn get_file_font(extn: &str) -> String {
let font = if constant::IMAGE_FORMATS.contains(&extn) {
"fa-regular fa-file-image"
} else {
Expand Down
12 changes: 9 additions & 3 deletions src/squire/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use actix_web::HttpRequest;

use crate::constant;


/// Logs connection information for an incoming HTTP request.
///
/// # Arguments
Expand All @@ -10,14 +11,19 @@ use crate::constant;
/// * `session` - Session struct that holds the `session_mapping` and `session_tracker` to handle sessions.
///
/// This function logs the host and user agent information of the incoming connection.
pub fn log_connection(request: &HttpRequest, session: &constant::Session) {
let host = request.connection_info().host().to_owned();
///
/// # Returns
///
/// Returns a tuple of the host, and the last streamed file path.
pub fn log_connection(request: &HttpRequest, session: &constant::Session) -> (String, String) {
let host = request.connection_info().host().to_string();
let mut tracker = session.tracker.lock().unwrap();
if tracker.get(&host).is_none() {
tracker.insert(request.connection_info().host().to_string(), "".to_string());
tracker.insert(host.clone(), "".to_string());
log::info!("Connection received from {}", host);
if let Some(user_agent) = request.headers().get("user-agent") {
log::info!("User agent: {}", user_agent.to_str().unwrap())
}
}
return (host.clone(), tracker.get(&host).map_or("".to_string(), |s| s.to_string()))
}
3 changes: 2 additions & 1 deletion src/squire/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub fn restricted(error: Template, username: &String, version: &String) -> HttpR
title => "RESTRICTED SECTION",
description => format!("This content is not accessible, as it does not belong to the user profile '{}'", username),
help => r"Lost your way?\n\nHit the HOME button to navigate back to home page.",
button_text => "HOME", button_link => "/home"
button_text => "HOME", button_link => "/home",
block_navigation => true
)).unwrap())
}
18 changes: 10 additions & 8 deletions src/templates/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub fn get_content() -> String {
r###"<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>RuStream - Self-hosted Streaming Engine - v{{ version }}</title>
<meta property="og:type" content="MediaStreaming">
<meta name="keywords" content="Python, streaming, fastapi, JavaScript, HTML, CSS">
Expand Down Expand Up @@ -80,15 +80,17 @@ pub fn get_content() -> String {
</button>
<h4>Click <a href="https://vigneshrao.com/contact">HERE</a> to reach out.</h4>
</body>
<!-- control the behavior of the browser's navigation without triggering a full page reload -->
<script>
document.addEventListener('DOMContentLoaded', function() {
history.pushState(null, document.title, location.href);
window.addEventListener('popstate', function (event) {
{% if block_navigation %}
<!-- control the behavior of the browser's navigation without triggering a full page reload -->
<script>
document.addEventListener('DOMContentLoaded', function() {
history.pushState(null, document.title, location.href);
window.addEventListener('popstate', function (event) {
history.pushState(null, document.title, location.href);
});
});
});
</script>
</script>
{% endif %}
</html>
"###.to_string()
}
6 changes: 3 additions & 3 deletions src/templates/landing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ pub fn get_content() -> String {
<div class="dropdown">
<button class="dropbtn"><i class="fa fa-user"></i></button>
<div class="dropdown-content">
<a onclick="goSecure()" style="cursor: pointer;"><i class="fa-solid fa-user-lock"></i> {{ user }}</a>
<a onclick="goProfile()" style="cursor: pointer;"><i class="fa-solid fa-user-lock"></i> {{ user }}</a>
<a onclick="logOut()" style="cursor: pointer"><i class="fa fa-sign-out"></i> logout</a>
</div>
</div>
Expand Down Expand Up @@ -287,8 +287,8 @@ pub fn get_content() -> String {
function goHome() {
window.location.href = "/home";
}
function goSecure() {
window.location.href = '/stream/{{ user }}_{{ secure_index }}';
function goProfile() {
window.location.href = '/profile';
}
function logOut() {
window.location.href = "/logout";
Expand Down
6 changes: 3 additions & 3 deletions src/templates/listing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ pub fn get_content() -> String {
<div class="dropdown">
<button class="dropbtn"><i class="fa fa-user"></i></button>
<div class="dropdown-content">
<a onclick="goSecure()" style="cursor: pointer;"><i class="fa-solid fa-user-lock"></i> {{ user }}</a>
<a onclick="goProfile()" style="cursor: pointer;"><i class="fa-solid fa-user-lock"></i> {{ user }}</a>
<a onclick="logOut()" style="cursor: pointer"><i class="fa fa-sign-out"></i> logout</a>
</div>
</div>
Expand Down Expand Up @@ -201,8 +201,8 @@ pub fn get_content() -> String {
function goHome() {
window.location.href = "/home";
}
function goSecure() {
window.location.href = '/stream/{{ user }}_{{ secure_index }}';
function goProfile() {
window.location.href = '/profile';
}
function logOut() {
window.location.href = "/logout";
Expand Down
2 changes: 1 addition & 1 deletion src/templates/logout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub fn get_content() -> String {
r###"<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>RuStream - Self-hosted Streaming Engine - v{{ version }}</title>
<meta property="og:type" content="MediaStreaming">
<meta name="keywords" content="Python, streaming, fastapi, JavaScript, HTML, CSS">
Expand Down
2 changes: 2 additions & 0 deletions src/templates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod session;
/// Error page template that is served as HTML response for any error message to be conveyed.
mod error;
mod upload;
mod profile;

/// Loads all the HTML templates' content into a Jinja Environment
///
Expand All @@ -29,5 +30,6 @@ pub fn environment() -> Arc<minijinja::Environment<'static>> {
env.add_template_owned("session", session::get_content()).unwrap();
env.add_template_owned("error", error::get_content()).unwrap();
env.add_template_owned("upload", upload::get_content()).unwrap();
env.add_template_owned("profile", profile::get_content()).unwrap();
Arc::new(env)
}
Loading

0 comments on commit 43777e8

Please sign in to comment.