diff --git a/build.rs b/build.rs index e3b726d..1dc7030 100644 --- a/build.rs +++ b/build.rs @@ -46,9 +46,11 @@ fn generate_web_files() { "commons/script.js", ].iter().for_each(|file_name| { let path = Path::new(file_name); + let src_path = src_dir.join(path); + println!("cargo:rerun-if-changed={}", src_path.to_str().unwrap()); let extension = path.extension().unwrap().to_str().unwrap(); - let mut content = read_to_string(src_dir.join(path)).unwrap(); - if extension == "css" { + let mut content = read_to_string(src_path).unwrap(); + if extension == "css" || file_name == &"login.html" { replace_fields(&mut content, fields); } if file_name == &"index.html" { @@ -59,7 +61,9 @@ fn generate_web_files() { dst.write(minified_content.as_bytes()).unwrap(); }); - let mut text_avatar = read_to_string("src/frontend/imgs/text_avatar.svg").unwrap(); + const TEXT_AVATAR_PATH: &str = "src/frontend/imgs/text_avatar.svg"; + let mut text_avatar = read_to_string(TEXT_AVATAR_PATH).unwrap(); + println!("cargo:rerun-if-changed={}", TEXT_AVATAR_PATH); replace_fields(&mut text_avatar, fields); File::create(out_dir.join("text_avatar.svg")).unwrap().write(text_avatar.as_bytes()).unwrap(); } diff --git a/src/frontend/commons/script.js b/src/frontend/commons/script.js index 278c951..04d9043 100644 --- a/src/frontend/commons/script.js +++ b/src/frontend/commons/script.js @@ -14,4 +14,54 @@ function generateAvatar(sessionId, name, timestamp) { let img = generateImgAvatar(); img.src = "/avatar/"+sessionId+"/"+name+"?"+timestamp; return img; +} + +function removePopup() { + let popups = document.querySelectorAll(".popup_background"); + if (popups.length > 0) { + popups[popups.length-1].remove(); + } +} +function showPopup(content, cancelable = true) { + let popup_background = document.createElement("div"); + popup_background.classList.add("popup_background"); + let popup = document.createElement("div"); + popup.classList.add("popup"); + if (cancelable) { + popup_background.onclick = function(e) { + if (e.target == popup_background) { + removePopup(); + } + }; + let close = document.createElement("button"); + close.classList.add("close"); + close.onclick = removePopup; + popup.appendChild(close); + } + popup.appendChild(content); + popup_background.appendChild(popup); + let main = document.querySelector("main"); + main.appendChild(popup_background); +} + +function uploadAvatar(event, onUploaded) { + let file = event.target.files[0]; + if (file.size < 10000000) { + let formData = new FormData(); + formData.append("avatar", file); + fetch("/set_avatar", {method: "POST", body: formData}).then(response => { + if (response.ok) { + onUploaded(); + } else { + console.log(response); + } + }); + } else { + let mainDiv = document.createElement("div"); + mainDiv.appendChild(generatePopupWarningTitle()); + let p = document.createElement("p"); + p.textContent = "Avatar cannot be larger than 10MB."; + mainDiv.appendChild(p); + showPopup(mainDiv); + } } \ No newline at end of file diff --git a/src/frontend/commons/style.css b/src/frontend/commons/style.css index 82224f7..bb40552 100644 --- a/src/frontend/commons/style.css +++ b/src/frontend/commons/style.css @@ -24,10 +24,97 @@ input[type="text"], input[type="password"] { width: 100%; margin: 0; } +input[type="file"] { + display: none; +} + +label { + cursor: pointer; +} .avatar { margin-right: .5em; width: 1.5em; height: 1.5em; border-radius: 50%; +} + +main.card { + max-width: 500px; + background-color: #2B2F31; + border-radius: 10px; + position: absolute; + top: 50%; + transform: translateY(-50%); + left: 0; + right: 0; + margin: auto; +} + +.popup { + position: absolute; + top: 50%; + transform: translateY(-50%); + left: 0; + right: 0; + margin: auto; + width: 40vw; + max-height: 90vh; + overflow: auto; + box-sizing: border-box; + padding: 20px 70px 10px; + background-color: #2B2F31; + border-radius: 10px; + font-size: 1.2em; +} +.popup:last-child::after { + content: ""; + display: block; + height: 20px; + width: 100%; +} +.popup_background { + height: 100%; + width: 100%; + position: absolute; + background-color: rgba(0, 0, 0, .5); + z-index: 2; +} +.popup .close { + background-color: unset; + position: absolute; + right: 0; + top: 6px; +} +.popup .close::after { + content: url("/static/imgs/icons/cancel"); + background-color: unset; +} + +#avatarContainer { + display: flex; + justify-content: center; + padding-bottom: 1.5em; +} +#avatarContainer .avatar { + margin-right: unset; +} +#avatarContainer label:hover .avatar { + opacity: .4; +} +#avatarContainer label { + position: relative; +} +#avatarContainer .avatar + p { + display: none; + position: absolute; + top: 50%; + transform: translateY(-50%); + left: 0; + right: 0; + margin: 0; + text-align: center; +} +#avatarContainer label:hover p { + display: block; } \ No newline at end of file diff --git a/src/frontend/imgs/icons/profile.svg b/src/frontend/imgs/icons/profile.svg new file mode 100644 index 0000000..98f8ddd --- /dev/null +++ b/src/frontend/imgs/icons/profile.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/index.css b/src/frontend/index.css index 463bb74..78622e9 100644 --- a/src/frontend/index.css +++ b/src/frontend/index.css @@ -48,9 +48,6 @@ button:hover::after { .classic_button:hover { background-color: var(--accent); } -input[type="file"] { - display: none; -} .file_picker { display: flex; align-items: center; @@ -60,57 +57,12 @@ input[type="file"] { content: url("/static/imgs/icons/attach/ACCENT_COLOR"); width: 2em; } -.popup { - position: absolute; - top: 50%; - transform: translateY(-50%); - left: 0; - right: 0; - margin: auto; - width: 40vw; - max-height: 90vh; - overflow: auto; - box-sizing: border-box; - padding: 20px 70px 10px; - background-color: #2B2F31; - border-radius: 10px; - font-size: 1.2em; -} -.popup:last-child::after { - content: ""; - display: block; - height: 20px; - width: 100%; -} -.popup_background { - height: 100%; - width: 100%; - position: absolute; - background-color: rgba(0, 0, 0, .5); - z-index: 2; -} -.popup .close { - background-color: unset; - position: absolute; - right: 0; - top: 6px; -} -.popup .close:hover { - background-color: unset; -} -.popup .close::after { - content: url("/static/imgs/icons/cancel"); - background-color: unset; -} .popup h2.warning::before { content: url("/static/imgs/icons/warning/ACCENT_COLOR"); width: 9%; display: inline-block; vertical-align: middle; } -label { - cursor: pointer; -} .switch_preference { display: flex; align-items: center; @@ -166,32 +118,9 @@ label { } #avatarContainer { position: relative; - padding-bottom: 1.5em; - display: flex; - justify-content: center; } #avatarContainer .avatar { font-size: 4em; - margin-right: unset; -} -#avatarContainer label:hover .avatar { - opacity: .4; -} -#avatarContainer>label { - position: relative; -} -#avatarContainer .avatar + p { - display: none; - position: absolute; - top: 50%; - transform: translateY(-50%); - left: 0; - right: 0; - margin: auto; - text-align: center; -} -#avatarContainer label:hover p { - display: block; } #removeAvatar { position: absolute; diff --git a/src/frontend/index.js b/src/frontend/index.js index 3257024..f0473b2 100644 --- a/src/frontend/index.js +++ b/src/frontend/index.js @@ -247,26 +247,10 @@ profile_div.onclick = function() { inputAvatar.accept = "image/*"; inputAvatar.id = "avatar_input"; inputAvatar.onchange = function(event) { - let file = event.target.files[0]; - if (file.size < 10000000) { - let formData = new FormData(); - formData.append("avatar", file); - fetch("/set_avatar", {method: "POST", body: formData}).then(response => { - if (response.ok) { - avatarTimestamps.set("self", Date.now()); - refreshSelfAvatar(); - } else { - console.log(response); - } - }); - } else { - let mainDiv = document.createElement("div"); - mainDiv.appendChild(generatePopupWarningTitle()); - let p = document.createElement("p"); - p.textContent = "Avatar cannot be larger than 10MB."; - mainDiv.appendChild(p); - showPopup(mainDiv); - } + uploadAvatar(event, function() { + avatarTimestamps.set("self", Date.now()); + refreshSelfAvatar(); + }); }; labelAvatar.appendChild(inputAvatar); labelAvatar.appendChild(generateSelfAvatar(avatarTimestamps.get("self"))); @@ -965,33 +949,6 @@ function displayHeader() { } } } -function showPopup(content, cancelable = true) { - let popup_background = document.createElement("div"); - popup_background.classList.add("popup_background"); - let popup = document.createElement("div"); - popup.classList.add("popup"); - if (cancelable) { - popup_background.onclick = function(e) { - if (e.target == popup_background) { - removePopup(); - } - }; - let close = document.createElement("button"); - close.classList.add("close"); - close.onclick = removePopup; - popup.appendChild(close); - } - popup.appendChild(content); - popup_background.appendChild(popup); - let main = document.querySelector("main"); - main.appendChild(popup_background); -} -function removePopup() { - let popups = document.querySelectorAll(".popup_background"); - if (popups.length > 0) { - popups[popups.length-1].remove(); - } -} function generatePopupWarningTitle() { let h2 = document.createElement("h2"); h2.classList.add("warning"); diff --git a/src/frontend/login.html b/src/frontend/login.html index 0fbb3c9..385c50f 100644 --- a/src/frontend/login.html +++ b/src/frontend/login.html @@ -14,14 +14,6 @@ background-color: black; } main { - max-width: 500px; - background-color: #2B2F31; - border-radius: 10px; - position: absolute; - top: 20vh; - left: 0; - right: 0; - margin: auto; padding-bottom: 20px; } h1, main>h3, #error_msg { @@ -40,6 +32,9 @@ font-weight: bold; margin-bottom: 0; } + .action_page>h2 { + margin-top: 0; + } input[type="text"], input[type="password"] { margin-bottom: 20px; } @@ -65,13 +60,11 @@ .avatar { width: 7em; height: 7em; - display: block; - margin: auto; } #identity h2 { text-align: center; } - p { + #error_msg { color: red; } label.checkbox { @@ -110,24 +103,41 @@ label.checkbox input:checked ~ .checkmark:after { display: block; } + #avatarContainer .avatar.unset { + border: 2px solid var(--accent); + } + #identity .avatar { + display: block; + margin: auto; + } -
+

AIRA

Local network secure P2P communications

ERROR_MSG

Login:

-
+
+ +

+
-

Create New Identity:

+

Create a new identity:

+
+ +
diff --git a/src/frontend/logout.html b/src/frontend/logout.html index 498fef6..d47eef4 100644 --- a/src/frontend/logout.html +++ b/src/frontend/logout.html @@ -14,15 +14,6 @@ background-color: black; } main { - max-width: 500px; - background-color: #2B2F31; - border-radius: 10px; - position: absolute; - top: 50%; - transform: translateY(-50%); - left: 0; - right: 0; - margin: auto; padding: 50px; } img { @@ -37,7 +28,7 @@ -
+

You've been successfully logged out.

diff --git a/src/main.rs b/src/main.rs index 4aade81..ce3331f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -309,32 +309,38 @@ fn is_authenticated(req: &HttpRequest) -> bool { } async fn handle_set_avatar(req: HttpRequest, mut payload: Multipart) -> HttpResponse { - if is_authenticated(&req) { - while let Ok(Some(mut field)) = payload.try_next().await { - let content_disposition = field.content_disposition().unwrap(); - if let Some(name) = content_disposition.get_name() { - if name == "avatar" { - let mut buffer = Vec::new(); - while let Some(Ok(chunk)) = field.next().await { - buffer.extend(chunk); - } - match image::guess_format(&buffer) { - Ok(format) => { - match image::load_from_memory_with_format(&buffer, format) { - Ok(image) => { - let (width, height) = image.dimensions(); - let image = if width < height { - image.crop_imm(0, (height-width)/2, width, width) - } else if width > height { - image.crop_imm((width-height)/2, 0, height, height) - } else { - image - }; - let mut avatar = Vec::new(); - image.write_to(&mut avatar, format).unwrap(); - let global_vars = req.app_data::>>>().unwrap(); - match global_vars.read().unwrap().session_manager.set_avatar(&avatar).await { + if let Ok(Some(mut field)) = payload.try_next().await { + let content_disposition = field.content_disposition().unwrap(); + if let Some(name) = content_disposition.get_name() { + if name == "avatar" { + let mut buffer = Vec::new(); + while let Some(Ok(chunk)) = field.next().await { + buffer.extend(chunk); + } + match image::guess_format(&buffer) { + Ok(format) => { + match image::load_from_memory_with_format(&buffer, format) { + Ok(image) => { + let (width, height) = image.dimensions(); + let image = if width < height { + image.crop_imm(0, (height-width)/2, width, width) + } else if width > height { + image.crop_imm((width-height)/2, 0, height, height) + } else { + image + }; + let mut avatar = Vec::new(); + image.write_to(&mut avatar, format).unwrap(); + let global_vars = req.app_data::>>>().unwrap(); + let session_manager = &global_vars.read().unwrap().session_manager; + let is_authenticated = is_authenticated(&req); + let is_running = session_manager.is_identity_loaded(); + if is_authenticated || !is_running { + match Identity::set_identity_avatar(&avatar) { Ok(_) => { + if is_authenticated && is_running { + session_manager.send_avatar(&avatar).await; + } return HttpResponse::Ok().finish(); } Err(e) => { @@ -343,11 +349,11 @@ async fn handle_set_avatar(req: HttpRequest, mut payload: Multipart) -> HttpResp } } } - Err(e) => print_error!(e) } + Err(e) => print_error!(e) } - Err(e) => print_error!(e) } + Err(e) => print_error!(e) } } } @@ -355,15 +361,18 @@ async fn handle_set_avatar(req: HttpRequest, mut payload: Multipart) -> HttpResp HttpResponse::BadRequest().finish() } -fn reply_with_avatar(avatar: Option>, name: &str) -> HttpResponse { +fn reply_with_avatar(avatar: Option>, name: Option<&str>) -> HttpResponse { match avatar { Some(avatar) => HttpResponse::Ok().content_type("image/*").body(avatar), - None => { - #[cfg(not(debug_assertions))] - let svg = include_str!(concat!(env!("OUT_DIR"), "/text_avatar.svg")); - #[cfg(debug_assertions)] - let svg = replace_fields("src/frontend/imgs/text_avatar.svg"); - HttpResponse::Ok().content_type("image/svg+xml").body(svg.replace("LETTER", &name.chars().nth(0).unwrap().to_string())) + None => match name { + Some(name) => { + #[cfg(not(debug_assertions))] + let svg = include_str!(concat!(env!("OUT_DIR"), "/text_avatar.svg")); + #[cfg(debug_assertions)] + let svg = replace_fields("src/frontend/imgs/text_avatar.svg"); + HttpResponse::Ok().content_type("image/svg+xml").body(svg.replace("LETTER", &name.chars().nth(0).unwrap().to_string())) + } + None => HttpResponse::InternalServerError().finish() } } } @@ -372,12 +381,12 @@ fn handle_avatar(req: HttpRequest) -> HttpResponse { let splits: Vec<&str> = req.path()[1..].split("/").collect(); if splits.len() == 2 { if splits[1] == "self" { - return reply_with_avatar(Identity::get_identity_avatar().ok(), &Identity::get_identity_name().unwrap()); + return reply_with_avatar(Identity::get_identity_avatar().ok(), Identity::get_identity_name().ok().as_deref()); } } else if splits.len() == 3 { if let Ok(session_id) = splits[1].parse() { let global_vars = req.app_data::>>>().unwrap(); - return reply_with_avatar(global_vars.read().unwrap().session_manager.get_avatar(&session_id), splits[2]); + return reply_with_avatar(global_vars.read().unwrap().session_manager.get_avatar(&session_id), Some(splits[2])); } } HttpResponse::BadRequest().finish() @@ -447,9 +456,7 @@ async fn handle_send_file(req: HttpRequest, mut payload: Multipart) -> HttpRespo loop { chunk_buffer.extend(&pending_buffer); pending_buffer.clear(); - println!("Calling next()"); while let Some(Ok(chunk)) = field.next().await { - println!("Not None"); if chunk_buffer.len()+chunk.len() <= constants::FILE_CHUNK_SIZE { chunk_buffer.extend(chunk); } else if chunk_buffer.len() == constants::FILE_CHUNK_SIZE { @@ -462,7 +469,6 @@ async fn handle_send_file(req: HttpRequest, mut payload: Multipart) -> HttpRespo break; } } - println!("May be None"); if !global_vars_read.session_manager.send_command(&session_id, SessionCommand::EncryptFileChunk{ plain_text: chunk_buffer.clone() }).await { @@ -546,6 +552,10 @@ fn on_identity_loaded(identity: Identity, global_vars: &Arc>) login(identity, global_vars) } +#[derive(Serialize, Deserialize)] +struct LoginParams { + password: String, +} fn handle_login(req: HttpRequest, mut params: web::Form) -> HttpResponse { let response = match Identity::load_identity(Some(params.password.as_bytes())) { Ok(identity) => { @@ -560,7 +570,7 @@ fn handle_login(req: HttpRequest, mut params: web::Form) -> HttpRes fn get_login_body(error_msg: Option<&str>) -> Result { #[cfg(debug_assertions)] - let html = fs::read_to_string("src/frontend/login.html").unwrap(); + let html = replace_fields("src/frontend/login.html"); #[cfg(not(debug_assertions))] let html = include_str!(concat!(env!("OUT_DIR"), "/login.html")); Ok(html @@ -570,8 +580,10 @@ fn get_login_body(error_msg: Option<&str>) -> Result { }) .replace("IDENTITY_NAME", &match Identity::get_identity_name() { Ok(name) => format!("\"{}\"", html_escape::encode_double_quoted_attribute(&name)), - Err(e) => { - print_error!(e); + Err(_) => { + if let Err(e) = Identity::remove_identity_avatar() { + print_error!(e); + } "null".to_owned() } } @@ -589,6 +601,12 @@ fn generate_login_response(error_msg: Option<&str>) -> HttpResponse { } } +#[derive(Serialize, Deserialize)] +struct CreateParams { + name: String, + password: String, + password_confirm: String, +} async fn handle_create(req: HttpRequest, mut params: web::Form) -> HttpResponse { let response = if params.password == params.password_confirm { match Identity::create_identidy( @@ -701,6 +719,7 @@ fn handle_static(req: HttpRequest) -> HttpResponse { "refresh" => Some(include_str!("frontend/imgs/icons/refresh.svg")), "info" => Some(include_str!("frontend/imgs/icons/info.svg")), "delete_conversation" => Some(include_str!("frontend/imgs/icons/delete_conversation.svg")), + "profile" => Some(include_str!("frontend/imgs/icons/profile.svg")), _ => None } { Some(body) => { @@ -796,18 +815,6 @@ async fn start_http_server(global_vars: Arc>) -> io::Result<( server.run().await } -#[derive(Serialize, Deserialize)] -struct LoginParams { - password: String, -} - -#[derive(Serialize, Deserialize)] -struct CreateParams { - name: String, - password: String, - password_confirm: String, -} - struct GlobalVars { session_manager: Arc, websocket_port: u16, diff --git a/src/session_manager.rs b/src/session_manager.rs index 1da4e0b..28c4f03 100644 --- a/src/session_manager.rs +++ b/src/session_manager.rs @@ -681,15 +681,13 @@ impl SessionManager { } #[allow(unused_must_use)] - pub async fn set_avatar(&self, avatar: &[u8]) -> Result<(), rusqlite::Error> { - Identity::set_identity_avatar(&avatar)?; + pub async fn send_avatar(&self, avatar: &[u8]) { let avatar_msg = protocol::avatar(&avatar); for sender in self.get_all_senders().into_iter() { sender.send(SessionCommand::Send { buff: avatar_msg.clone() }).await; } - Ok(()) } #[allow(unused_must_use)] @@ -705,17 +703,15 @@ impl SessionManager { } #[allow(unused_must_use)] - pub async fn change_name(&self, new_name: String) -> Result { + pub async fn change_name(&self, new_name: String) -> Result<(), rusqlite::Error> { let telling_name = protocol::name(&new_name); - let result = self.identity.write().unwrap().as_mut().unwrap().change_name(new_name); - if result.is_ok() { - for sender in self.get_all_senders().into_iter() { - sender.send(SessionCommand::Send { - buff: telling_name.clone() - }).await; - } + self.identity.write().unwrap().as_mut().unwrap().change_name(new_name)?; + for sender in self.get_all_senders().into_iter() { + sender.send(SessionCommand::Send { + buff: telling_name.clone() + }).await; } - result + Ok(()) } #[allow(unused_must_use)]