diff --git a/Cargo.lock b/Cargo.lock index ffcff3dd..fb873a7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2636,6 +2636,7 @@ dependencies = [ "sqlx-rt", "stringprep", "thiserror", + "time 0.2.26", "url", "webpki", "webpki-roots", diff --git a/Cargo.toml b/Cargo.toml index 2d34dd1c..52f7bffd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ cache-buster = { version = "0.2.0", git = "https://github.com/realaravinth/cache futures = "0.3.14" -sqlx = { version = "0.4.0", features = [ "runtime-actix-rustls", "postgres" ] } +sqlx = { version = "0.4.0", features = [ "runtime-actix-rustls", "postgres", "time" ] } argon2-creds = { version = "0.2", git = "https://github.com/realaravinth/argon2-creds", commit = "61f2d1d" } config = "0.11" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..038034fe --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 89 diff --git a/src/api/v1/account/secret.rs b/src/api/v1/account/secret.rs index 5ed0e2c4..c125d275 100644 --- a/src/api/v1/account/secret.rs +++ b/src/api/v1/account/secret.rs @@ -29,7 +29,10 @@ pub struct Secret { pub secret: String, } -async fn get_secret(id: Identity, data: web::Data) -> ServiceResult { +async fn get_secret( + id: Identity, + data: web::Data, +) -> ServiceResult { let username = id.identity().unwrap(); let secret = sqlx::query_as!( @@ -43,7 +46,10 @@ async fn get_secret(id: Identity, data: web::Data) -> ServiceResult) -> ServiceResult { +async fn update_user_secret( + id: Identity, + data: web::Data, +) -> ServiceResult { let username = id.identity().unwrap(); let mut secret; diff --git a/src/api/v1/mcaptcha/levels.rs b/src/api/v1/mcaptcha/levels.rs index 2ffb2af5..aa18fc85 100644 --- a/src/api/v1/mcaptcha/levels.rs +++ b/src/api/v1/mcaptcha/levels.rs @@ -250,7 +250,11 @@ pub struct I32Levels { pub visitor_threshold: i32, } -async fn get_levels_util(key: &str, username: &str, data: &Data) -> ServiceResult> { +async fn get_levels_util( + key: &str, + username: &str, + data: &Data, +) -> ServiceResult> { let levels = sqlx::query_as!( I32Levels, "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE diff --git a/src/api/v1/mcaptcha/mcaptcha.rs b/src/api/v1/mcaptcha/mcaptcha.rs index c96f5e1e..e08d484d 100644 --- a/src/api/v1/mcaptcha/mcaptcha.rs +++ b/src/api/v1/mcaptcha/mcaptcha.rs @@ -299,7 +299,8 @@ mod tests { ) .await; assert_eq!(update_token_resp.status(), StatusCode::OK); - let updated_token: MCaptchaDetails = test::read_body_json(update_token_resp).await; + let updated_token: MCaptchaDetails = + test::read_body_json(update_token_resp).await; // get token key with updated key let get_token_resp = test::call_service( @@ -312,7 +313,8 @@ mod tests { assert_eq!(get_token_resp.status(), StatusCode::OK); // check if they match - let mut get_token_key: MCaptchaDetails = test::read_body_json(get_token_resp).await; + let mut get_token_key: MCaptchaDetails = + test::read_body_json(get_token_resp).await; assert_eq!(get_token_key.key, updated_token.key); get_token_key.key = "nonexistent".into(); diff --git a/src/api/v1/notifications/get.rs b/src/api/v1/notifications/get.rs new file mode 100644 index 00000000..b1d6e7f6 --- /dev/null +++ b/src/api/v1/notifications/get.rs @@ -0,0 +1,147 @@ +/* +* Copyright (C) 2021 Aravinth Manivannan +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ + +use actix_identity::Identity; +use actix_web::{web, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; +use sqlx::types::time::OffsetDateTime; + +use crate::errors::*; +use crate::Data; + +pub struct Notification { + pub name: String, + pub heading: String, + pub message: String, + pub received: OffsetDateTime, + pub id: i32, +} + +#[derive(Deserialize, Serialize)] +pub struct NotificationResp { + pub name: String, + pub heading: String, + pub message: String, + pub received: i64, + pub id: i32, +} + +impl From for NotificationResp { + fn from(n: Notification) -> Self { + NotificationResp { + name: n.name, + heading: n.heading, + received: n.received.unix_timestamp(), + id: n.id, + message: n.message, + } + } +} +/// route handler that gets all unread notifications +pub async fn get_notification( + data: web::Data, + id: Identity, +) -> ServiceResult { + let receiver = id.identity().unwrap(); + // TODO handle error where payload.to doesnt exist + + let mut notifications = sqlx::query_file_as!( + Notification, + "src/api/v1/notifications/get_all_unread.sql", + &receiver + ) + .fetch_all(&data.db) + .await?; + + let resp: Vec = notifications + .drain(0..) + .map(|x| { + let y: NotificationResp = x.into(); + y + }) + .collect(); + + Ok(HttpResponse::Ok().json(resp)) +} + +#[cfg(test)] +mod tests { + use actix_web::http::{header, StatusCode}; + use actix_web::test; + + use super::*; + use crate::api::v1::notifications::add::AddNotification; + use crate::tests::*; + use crate::*; + + #[actix_rt::test] + async fn notification_get_works() { + const NAME1: &str = "notifuser12"; + const NAME2: &str = "notiuser22"; + const PASSWORD: &str = "longpassworddomain"; + const EMAIL1: &str = "testnotification12@a.com"; + const EMAIL2: &str = "testnotification22@a.com"; + const HEADING: &str = "testing notifications get"; + const MESSAGE: &str = "testing notifications get message"; + + { + let data = Data::new().await; + delete_user(NAME1, &data).await; + delete_user(NAME2, &data).await; + } + + register_and_signin(NAME1, EMAIL1, PASSWORD).await; + register_and_signin(NAME2, EMAIL2, PASSWORD).await; + let (data, _creds, signin_resp) = signin(NAME1, PASSWORD).await; + let (_data, _creds2, signin_resp2) = signin(NAME2, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let cookies2 = get_cookie!(signin_resp2); + let mut app = get_app!(data).await; + + let msg = AddNotification { + to: NAME2.into(), + heading: HEADING.into(), + message: MESSAGE.into(), + }; + + let send_notification_resp = test::call_service( + &mut app, + post_request!(&msg, V1_API_ROUTES.notifications.add) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(send_notification_resp.status(), StatusCode::OK); + + let get_notifications_resp = test::call_service( + &mut app, + test::TestRequest::get() + .uri(V1_API_ROUTES.notifications.get) + .cookie(cookies2.clone()) + .to_request(), + ) + .await; + assert_eq!(get_notifications_resp.status(), StatusCode::OK); + + let mut notifications: Vec = + test::read_body_json(get_notifications_resp).await; + let notification = notifications.pop().unwrap(); + assert_eq!(notification.name, NAME1); + assert_eq!(notification.message, MESSAGE); + assert_eq!(notification.heading, HEADING); + } +} diff --git a/src/api/v1/notifications/get_all_unread.sql b/src/api/v1/notifications/get_all_unread.sql new file mode 100644 index 00000000..49fd614e --- /dev/null +++ b/src/api/v1/notifications/get_all_unread.sql @@ -0,0 +1,24 @@ +-- gets all unread notifications a user has +SELECT + mcaptcha_notifications.id, + mcaptcha_notifications.heading, + mcaptcha_notifications.message, + mcaptcha_notifications.received, + mcaptcha_users.name +FROM + mcaptcha_notifications +INNER JOIN + mcaptcha_users +ON + mcaptcha_notifications.tx = mcaptcha_users.id +WHERE + mcaptcha_notifications.rx = ( + SELECT + id + FROM + mcaptcha_users + WHERE + name = $1 + ) +AND + mcaptcha_notifications.read IS NULL; diff --git a/src/api/v1/notifications/mod.rs b/src/api/v1/notifications/mod.rs index 073dbfa8..39328662 100644 --- a/src/api/v1/notifications/mod.rs +++ b/src/api/v1/notifications/mod.rs @@ -16,6 +16,7 @@ */ mod add; +mod get; pub mod routes { @@ -30,7 +31,7 @@ pub mod routes { Notifications { add: "/api/v1/notifications/add", mark_read: "/api/v1/notifications/read/", - get: "/api/v1/notifications/get/", + get: "/api/v1/notifications/get", } } } @@ -46,4 +47,11 @@ pub fn services(cfg: &mut actix_web::web::ServiceConfig) { Methods::ProtectPost, add::add_notification ); + + define_resource!( + cfg, + V1_API_ROUTES.notifications.get, + Methods::ProtectGet, + get::get_notification + ); } diff --git a/src/api/v1/pow/get_config.rs b/src/api/v1/pow/get_config.rs index 00133485..7aa55254 100644 --- a/src/api/v1/pow/get_config.rs +++ b/src/api/v1/pow/get_config.rs @@ -17,7 +17,9 @@ use actix::prelude::*; use actix_web::{web, HttpResponse, Responder}; -use m_captcha::{defense::LevelBuilder, master::AddSiteBuilder, DefenseBuilder, MCaptchaBuilder}; +use m_captcha::{ + defense::LevelBuilder, master::AddSiteBuilder, DefenseBuilder, MCaptchaBuilder, +}; use serde::{Deserialize, Serialize}; use super::record_fetch; diff --git a/src/api/v1/pow/verify_pow.rs b/src/api/v1/pow/verify_pow.rs index ea468584..989bedcd 100644 --- a/src/api/v1/pow/verify_pow.rs +++ b/src/api/v1/pow/verify_pow.rs @@ -80,7 +80,8 @@ mod tests { let get_config_resp = test::call_service( &mut app, - post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config).to_request(), + post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config) + .to_request(), ) .await; assert_eq!(get_config_resp.status(), StatusCode::OK); @@ -119,7 +120,9 @@ mod tests { err.error, format!( "{}", - ServiceError::CaptchaError(m_captcha::errors::CaptchaError::StringNotFound) + ServiceError::CaptchaError( + m_captcha::errors::CaptchaError::StringNotFound + ) ) ); diff --git a/src/api/v1/pow/verify_token.rs b/src/api/v1/pow/verify_token.rs index 19d93986..121a5513 100644 --- a/src/api/v1/pow/verify_token.rs +++ b/src/api/v1/pow/verify_token.rs @@ -127,7 +127,8 @@ mod tests { ) .await; assert_eq!(validate_client_token.status(), StatusCode::OK); - let resp: CaptchaValidateResp = test::read_body_json(validate_client_token).await; + let resp: CaptchaValidateResp = + test::read_body_json(validate_client_token).await; assert!(resp.valid); // string not found diff --git a/src/api/v1/tests/protected.rs b/src/api/v1/tests/protected.rs index 7175af9d..c2755468 100644 --- a/src/api/v1/tests/protected.rs +++ b/src/api/v1/tests/protected.rs @@ -58,7 +58,8 @@ async fn protected_routes_work() { for url in get_protected_urls.iter() { let resp = - test::call_service(&mut app, test::TestRequest::get().uri(url).to_request()).await; + test::call_service(&mut app, test::TestRequest::get().uri(url).to_request()) + .await; assert_eq!(resp.status(), StatusCode::FOUND); let authenticated_resp = test::call_service( diff --git a/src/docs.rs b/src/docs.rs index e28c255c..27c3b9f2 100644 --- a/src/docs.rs +++ b/src/docs.rs @@ -124,8 +124,11 @@ mod tests { let uri = format!("{}{}", DOCS.home, FILE); - let resp = - test::call_service(&mut app, test::TestRequest::get().uri(&uri).to_request()).await; + let resp = test::call_service( + &mut app, + test::TestRequest::get().uri(&uri).to_request(), + ) + .await; assert_eq!(resp.status(), StatusCode::OK); } } diff --git a/src/main.rs b/src/main.rs index 8412ebdb..09bd79d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,8 +18,8 @@ use std::env; use actix_identity::{CookieIdentityPolicy, IdentityService}; use actix_web::{ - client::Client, error::InternalError, http::StatusCode, middleware as actix_middleware, - web::JsonConfig, App, HttpServer, + client::Client, error::InternalError, http::StatusCode, + middleware as actix_middleware, web::JsonConfig, App, HttpServer, }; use lazy_static::lazy_static; use log::info; @@ -51,8 +51,10 @@ lazy_static! { pub static ref SETTINGS: Settings = Settings::new().unwrap(); pub static ref S: String = env::var("S").unwrap(); pub static ref FILES: FileMap = FileMap::new(); - pub static ref JS: &'static str = FILES.get("./static-assets/bundle/bundle.js").unwrap(); - pub static ref CSS: &'static str = FILES.get("./static-assets/bundle/main.css").unwrap(); + pub static ref JS: &'static str = + FILES.get("./static-assets/bundle/bundle.js").unwrap(); + pub static ref CSS: &'static str = + FILES.get("./static-assets/bundle/main.css").unwrap(); } pub static OPEN_API_DOC: &str = env!("OPEN_API_DOCS"); @@ -61,7 +63,6 @@ pub static VERSION: &str = env!("CARGO_PKG_VERSION"); pub static PKG_NAME: &str = env!("CARGO_PKG_NAME"); pub static PKG_DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); pub static PKG_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE"); -pub static VERIFICATION_PATH: &str = "mcaptchaVerificationChallenge.json"; pub const CACHE_AGE: u32 = 365 * 24 * 3600; diff --git a/src/pages/mod.rs b/src/pages/mod.rs index ac351821..05ebc0ba 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -70,8 +70,11 @@ mod tests { ]; for url in urls.iter() { - let resp = - test::call_service(&mut app, test::TestRequest::get().uri(url).to_request()).await; + let resp = test::call_service( + &mut app, + test::TestRequest::get().uri(url).to_request(), + ) + .await; assert_eq!(resp.status(), StatusCode::FOUND); let authenticated_resp = test::call_service( @@ -95,8 +98,11 @@ mod tests { let urls = vec![PAGES.auth.login, PAGES.auth.join]; for url in urls.iter() { - let resp = - test::call_service(&mut app, test::TestRequest::get().uri(url).to_request()).await; + let resp = test::call_service( + &mut app, + test::TestRequest::get().uri(url).to_request(), + ) + .await; assert_eq!(resp.status(), StatusCode::OK); } diff --git a/src/pages/panel/sitekey/list.rs b/src/pages/panel/sitekey/list.rs index 1072927b..e7c4a41a 100644 --- a/src/pages/panel/sitekey/list.rs +++ b/src/pages/panel/sitekey/list.rs @@ -38,7 +38,10 @@ impl IndexPage { } /// render a list of all sitekeys that a user has -pub async fn list_sitekeys(data: web::Data, id: Identity) -> PageResult { +pub async fn list_sitekeys( + data: web::Data, + id: Identity, +) -> PageResult { let res = get_list_sitekeys(&data, &id).await?; let body = IndexPage::new(res).render_once().unwrap(); Ok(HttpResponse::Ok() diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 09b3aee5..205df515 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -94,7 +94,10 @@ pub async fn register<'a>(name: &'a str, email: &str, password: &str) { } /// signin util -pub async fn signin<'a>(name: &'a str, password: &str) -> (data::Data, Login, ServiceResponse) { +pub async fn signin<'a>( + name: &'a str, + password: &str, +) -> (data::Data, Login, ServiceResponse) { let data = Data::new().await; let mut app = get_app!(data).await;