Pour tout problème contactez-nous par mail : support@froggit.fr | La FAQ :grey_question: | Rejoignez-nous sur le Chat :speech_balloon:

Skip to content
Snippets Groups Projects
Commit a996871e authored by Pierre Jarriges's avatar Pierre Jarriges
Browse files

Merge branch 'dev' into 'master'

Dev

See merge request !1
parents d30c3361 b68000c1
No related branches found
No related tags found
No related merge requests found
mod administrator;
mod article;
pub use administrator::*;
pub use article::*;
use crate::AppState;
use actix_web::http::Cookie;
use serde::{Deserialize, Serialize};
use wither::{
bson::{doc, oid::ObjectId},
prelude::Model,
};
#[derive(Debug, Serialize, Deserialize)]
/// The data type that must sent by form data POST to authenticate an administrator.
pub struct AdminAuthCredentials {
pub username: String,
pub password: String,
}
#[derive(Debug, Deserialize, Serialize, Model)]
#[model(index(
keys = r#"doc!{"email": 1, "username": 1}"#,
options = r#"doc!{"unique": true}"#
))]
/// An administrator is a user with registered authentication credentials access right to the admin-panel and has ability to perform admin actions such as gam review, moderation, etc.
pub struct Administrator {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<ObjectId>,
pub username: String,
pub password_hash: String,
pub auth_token: Option<String>,
}
impl Administrator {
/// Creates an administrator with values for username and password.
/// The auth_token fields remains None as it must be created if the user authenticates itself with the provided credentials
/// The password is stored as password_hash, it is encrypted with the AppState::Encryption.
pub fn from_values(app_state: &AppState, username: String, password: String) -> Self {
Administrator {
id: None,
password_hash: app_state.encryption.encrypt(&password),
username,
auth_token: None,
}
}
/// Performs authentication with form data <username, password>.
/// Returns a Result with either an authenticated Administrator instance or an error.
pub async fn authenticated(
app_state: &AppState,
credentials: AdminAuthCredentials,
) -> Result<Self, ()> {
let filter_doc = doc! {
"password_hash": app_state.encryption.encrypt(&credentials.password),
"username": credentials.username
};
match Administrator::find_one(&app_state.db, filter_doc, None).await {
Ok(user_option) => match user_option {
Some(admin) => Ok(admin),
None => Err(()),
},
Err(_) => Err(()),
}
}
/// Performs authenticattion with auth cookie. The cookie value must match the Administrator auth_token value.
/// Returns a result with either the authenticated admin, or an empty Err.
pub async fn authenticated_with_cookie(
app_state: &AppState,
auth_cookie: &Cookie<'_>,
) -> Result<Self, ()> {
let cookie_value = auth_cookie.value().to_string();
let filter_doc = doc! {
"auth_token": app_state.encryption.encrypt(&cookie_value),
};
match Administrator::find_one(&app_state.db, filter_doc, None).await {
Ok(user_option) => match user_option {
Some(admin) => Ok(admin),
None => Err(()),
},
Err(_) => Err(()),
}
}
}
#[cfg(test)]
use chrono::Utc;
use serde::{Deserialize, Serialize};
use wither::{
bson::{doc, oid::ObjectId, DateTime},
prelude::Model,
};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ArticleDetail {
pub label: String,
pub value: String,
}
#[derive(Debug, Serialize, Deserialize, Model, Clone)]
#[model(index(keys = r#"doc!{"title": 1}"#, options = r#"doc!{"unique": true}"#))]
pub struct Article {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<ObjectId>,
pub title: String,
pub subtitle: String,
pub date: Option<DateTime>,
pub body: String,
pub details: Vec<ArticleDetail>,
pub images: Vec<String>,
pub category: String,
pub locale: String,
}
impl Article {
#[cfg(test)]
pub fn test_article() -> Self {
Article {
id: None,
title: "Test Article".to_string(),
subtitle: "An article for testing".to_string(),
date: Some(DateTime(Utc::now())),
body: "blablabla".to_string(),
details: vec![
ArticleDetail {
label: "A label".to_string(),
value: "A value".to_string(),
},
ArticleDetail {
label: "Another label".to_string(),
value: "Another value".to_string(),
},
],
images: vec!["an_image.png".to_string()],
category: "testing".to_string(),
locale: "fr".to_string(),
}
}
}
mod admin_auth;
mod articles;
pub use admin_auth::*;
pub use articles::*;
use crate::{middleware::AuthenticatedAdminMiddleware, model::AdminAuthCredentials, AppState};
use actix_web::{
post,
web::{Data, Form},
HttpMessage, HttpRequest, HttpResponse, Responder,
};
/// Performs administrator authentication from form data
/// If the authentication succeed, a cookie with an auth token is returned
/// If not, 401 is returned and if an auth cookie is found it is deleted.
#[post("/admin-auth")]
pub async fn admin_authentication<'a>(
app_state: Data<AppState>,
auth_mw: Data<AuthenticatedAdminMiddleware<'a>>,
req: HttpRequest,
form_data: Form<AdminAuthCredentials>,
) -> impl Responder {
let cookie_opt = auth_mw.exec(&app_state, &req, Some(form_data)).await;
match cookie_opt {
Ok(cookie) => HttpResponse::Accepted().cookie(cookie).finish(),
Err(_) => {
return match req.cookie(auth_mw.cookie_name) {
Some(c) => {
// Invalidate auth_cookie if auth failed in any way
HttpResponse::Unauthorized().del_cookie(&c).finish()
}
None => HttpResponse::Unauthorized().finish(),
};
}
}
}
/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
* _______ ______ ______ _______ *@@
* |__ __@ | ____@ / ____@ |__ __@ *@@
* | @ | @__ \_ @_ | @ *@@
* | @ | __@ \ @_ | @ *@@
* | @ | @___ ____\ @ | @ *@@
* |__@ |______@ \______@ |__@ *@@
* *@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@*/
#[cfg(test)]
mod test_admin_auth {
use super::*;
use crate::model::Administrator;
use actix_web::{
http::{Method, StatusCode},
test,
web::Data,
App,
};
use futures::stream::StreamExt;
use wither::prelude::Model;
#[tokio::test]
async fn test_admin_auth() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let admin_user = Administrator::find(&app_state.db, None, None)
.await
.unwrap()
.next()
.await
.unwrap()
.unwrap(); // Get the first admin user we find. At least one should exist.
let password = app_state.encryption.decrypt(&admin_user.password_hash);
let username = admin_user.username.to_owned();
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.service(admin_authentication),
)
.await;
let req = test::TestRequest::with_uri("/admin-auth")
.method(Method::POST)
.set_form(&AdminAuthCredentials { username, password })
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::ACCEPTED);
}
#[tokio::test]
async fn test_admin_auth_unauthorized() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.service(admin_authentication),
)
.await;
let req = test::TestRequest::with_uri("/admin-auth")
.method(Method::POST)
.set_form(&AdminAuthCredentials {
username: String::from("whatever"),
password: String::from("whatever"),
})
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
}
use crate::{middleware::AuthenticatedAdminMiddleware, model::Article, AppState};
use actix_web::{
delete, get, post, put,
web::{Data, Form, Json, Path},
HttpRequest, HttpResponse, Responder,
};
use chrono::Utc;
use futures::stream::StreamExt;
use serde::{Deserialize, Serialize};
use wither::{
bson::{doc, oid::ObjectId, DateTime},
mongodb::Collection,
prelude::Model,
};
#[derive(Deserialize, Serialize)]
pub struct ArticleTitleFormData {
pub title: String,
}
fn get_collection(app_state: &AppState) -> Collection<Article> {
app_state.db.collection_with_type::<Article>("articles")
}
#[post("/post-article")]
pub async fn post_article(
app_state: Data<AppState>,
article_data: Json<Article>,
middleware: Data<AuthenticatedAdminMiddleware<'_>>,
req: HttpRequest,
) -> impl Responder {
if middleware.exec(&app_state, &req, None).await.is_err() {
return HttpResponse::Unauthorized().finish();
}
let mut article_data = article_data.into_inner();
article_data.date = Some(DateTime(Utc::now()));
match get_collection(&app_state)
.insert_one(article_data, None)
.await
{
Ok(res) => HttpResponse::Created().json(res),
Err(e) => {
HttpResponse::InternalServerError().body(format!("Error inserting new article {:?}", e))
}
}
}
#[put("/update-article/{article_id}")]
pub async fn update_article(
app_state: Data<AppState>,
article_data: Json<Article>,
middleware: Data<AuthenticatedAdminMiddleware<'_>>,
article_id: Path<String>,
req: HttpRequest,
) -> impl Responder {
if middleware.exec(&app_state, &req, None).await.is_err() {
return HttpResponse::Unauthorized().finish();
}
let article_id = match ObjectId::with_string(&article_id.into_inner()) {
Ok(id) => id,
Err(_) => {
return HttpResponse::BadRequest()
.body("Failed to convert article_id to ObjectId. String may be malformed")
}
};
let mut article_data = article_data.into_inner();
article_data.date = Some(DateTime(Utc::now()));
match get_collection(&app_state)
.find_one_and_replace(doc! {"_id": &article_id}, article_data, None)
.await
{
Ok(res) => HttpResponse::Ok().json(res.unwrap()),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
#[delete("/delete-article/{article_id}")]
pub async fn delete_article(
app_state: Data<AppState>,
middleware: Data<AuthenticatedAdminMiddleware<'_>>,
article_id: Path<String>,
req: HttpRequest,
) -> impl Responder {
if middleware.exec(&app_state, &req, None).await.is_err() {
return HttpResponse::Unauthorized().finish();
}
let article_id = match ObjectId::with_string(&article_id.into_inner()) {
Ok(id) => id,
Err(_) => {
return HttpResponse::BadRequest()
.body("Failed to convert article_id to ObjectId. String may be malformed")
}
};
match get_collection(&app_state)
.find_one_and_delete(doc! {"_id": &article_id}, None)
.await
{
Ok(_) => HttpResponse::Accepted().body("Article was deleted"),
Err(e) => HttpResponse::InternalServerError().body(&format!("{:?}", e)),
}
}
#[get("/articles/{category}")]
pub async fn get_articles_by_category(
app_state: Data<AppState>,
category: Path<String>,
) -> impl Responder {
match get_collection(&app_state)
.find(doc! {"category": category.into_inner()}, None)
.await
{
Ok(mut cursor) => {
let mut results: Vec<Article> = Vec::new();
while let Some(result) = cursor.next().await {
match result {
Ok(article) => {
results.push(article);
}
Err(_) => {
return HttpResponse::InternalServerError().finish();
}
}
}
HttpResponse::Ok().json(results)
}
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
#[get("/article/{article_id}")]
pub async fn get_article(app_state: Data<AppState>, article_id: Path<String>) -> impl Responder {
let article_id = match ObjectId::with_string(&article_id.into_inner()) {
Ok(id) => id,
Err(_) => {
return HttpResponse::BadRequest()
.body("Failed to convert article_id to ObjectId. String may be malformed")
}
};
match Article::find_one(&app_state.db, doc! {"_id":&article_id}, None).await {
Ok(art) => {
if art.is_none() {
return HttpResponse::NotFound().body("Article was not found");
}
HttpResponse::Ok().json(art)
}
Err(e) => HttpResponse::InternalServerError().body(format!("Database error: {:#?}", e)),
}
}
#[post("/article-by-title")]
pub async fn get_article_by_title(
app_state: Data<AppState>,
form_data: Form<ArticleTitleFormData>,
) -> impl Responder {
let title = form_data.into_inner().title;
match Article::find_one(&app_state.db, doc! {"title":title}, None).await {
Ok(art) => {
if art.is_none() {
return HttpResponse::NotFound().body("Article was not found");
}
HttpResponse::Ok().json(art)
}
Err(e) => HttpResponse::InternalServerError().body(format!("Database error: {:#?}", e)),
}
}
/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
* _______ ______ ______ _______ *@@
* |__ __@ | ____@ / ____@ |__ __@ *@@
* | @ | @__ \_ @_ | @ *@@
* | @ | __@ \ @_ | @ *@@
* | @ | @___ ____\ @ | @ *@@
* |__@ |______@ \______@ |__@ *@@
* *@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@*/
#[cfg(test)]
mod test_articles {
use super::*;
use crate::middleware::get_auth_cookie;
use crate::model::{AdminAuthCredentials, Administrator};
use actix_web::{
http::{Method, StatusCode},
test,
web::Bytes,
App,
};
use wither::bson::Bson;
async fn insert_test_article(
app_state: &AppState,
test_article: Article,
) -> Result<(ObjectId, String), String> {
let title = test_article.title.to_owned();
match get_collection(&app_state)
.insert_one(test_article, None)
.await
{
Ok(inserted) => match inserted.inserted_id {
Bson::ObjectId(id) => Ok((id, title)),
_ => Err(String::from("Failed to parse inserted_id")),
},
Err(e) => Err(format!("{:?}", e)),
}
}
async fn delete_test_article(
app_state: &AppState,
article_id: &ObjectId,
) -> Result<i64, String> {
match get_collection(&app_state)
.delete_one(doc! {"_id": article_id}, None)
.await
{
Ok(delete_result) => Ok(delete_result.deleted_count),
Err(e) => Err(format!("{:?}", e)),
}
}
async fn get_authenticated_admin(app_state: &AppState) -> Administrator {
Administrator::authenticated(
app_state,
AdminAuthCredentials {
username: app_state.env.default_admin_username.to_owned(),
password: app_state.env.default_admin_password.to_owned(),
},
)
.await
.unwrap()
}
#[tokio::test]
async fn test_post_article() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.service(post_article),
)
.await;
let article = Article::test_article();
let admin_user = get_authenticated_admin(&app_state).await;
let req = test::TestRequest::with_uri("/post-article")
.method(Method::POST)
.header("Content-Type", "application/json")
.header("Accept", "text/html")
.cookie(get_auth_cookie(
"kuadrado-admin-auth",
app_state
.encryption
.decrypt(&admin_user.auth_token.unwrap())
.to_owned(),
))
.set_payload(Bytes::from(serde_json::to_string(&article).unwrap()))
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::CREATED);
let find_inserted = Article::find_one(&app_state.db, doc! {"title": &article.title}, None)
.await
.unwrap();
assert!(find_inserted.is_some());
assert_eq!(find_inserted.unwrap().title, article.title);
get_collection(&app_state)
.delete_one(doc! {"title": article.title}, None)
.await
.unwrap();
}
#[tokio::test]
async fn test_post_article_unauthorized() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.service(post_article),
)
.await;
let article = Article::test_article();
let req = test::TestRequest::with_uri("/post-article")
.method(Method::POST)
.header("Content-Type", "application/json")
.header("Accept", "text/html")
.cookie(get_auth_cookie(
"wrong-cookie",
app_state.encryption.random_ascii_lc_string(32),
))
.set_payload(Bytes::from(serde_json::to_string(&article).unwrap()))
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_update_article() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.service(update_article),
)
.await;
let mut article = Article::test_article();
let (article_id, _) = insert_test_article(&app_state, article.clone())
.await
.unwrap();
article.title = "changed title".to_string();
let admin_user = get_authenticated_admin(&app_state).await;
let req = test::TestRequest::with_uri(
format!("/update-article/{}", article_id.to_hex()).as_str(),
)
.method(Method::PUT)
.header("Content-Type", "application/json")
.header("Accept", "text/html")
.cookie(get_auth_cookie(
"kuadrado-admin-auth",
app_state
.encryption
.decrypt(&admin_user.auth_token.unwrap())
.to_owned(),
))
.set_payload(Bytes::from(serde_json::to_string(&article).unwrap()))
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let find_inserted = Article::find_one(&app_state.db, doc! {"_id": &article_id}, None)
.await
.unwrap();
assert!(find_inserted.is_some());
assert_eq!(find_inserted.unwrap().title, "changed title");
let del_count = delete_test_article(&app_state, &article_id).await.unwrap();
assert_eq!(del_count, 1);
}
#[tokio::test]
async fn test_update_article_unauthorized() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.service(update_article),
)
.await;
let article = Article::test_article();
let req = test::TestRequest::with_uri(
format!("/update-article/{}", ObjectId::new().to_hex()).as_str(),
)
.method(Method::PUT)
.header("Content-Type", "application/json")
.header("Accept", "text/html")
.cookie(get_auth_cookie(
"wrong-cookie",
app_state.encryption.random_ascii_lc_string(32),
))
.set_payload(Bytes::from(serde_json::to_string(&article).unwrap()))
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_delete_article() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.service(delete_article),
)
.await;
let article = Article::test_article();
let (article_id, _) = insert_test_article(&app_state, article.clone())
.await
.unwrap();
let admin_user = get_authenticated_admin(&app_state).await;
let req = test::TestRequest::with_uri(
format!("/delete-article/{}", article_id.to_hex()).as_str(),
)
.method(Method::DELETE)
.cookie(get_auth_cookie(
"kuadrado-admin-auth",
app_state
.encryption
.decrypt(&admin_user.auth_token.unwrap())
.to_owned(),
))
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::ACCEPTED);
let find_inserted = Article::find_one(&app_state.db, doc! {"_id": &article_id}, None)
.await
.unwrap();
assert!(find_inserted.is_none());
}
#[tokio::test]
async fn test_delete_article_unauthorized() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.service(delete_article),
)
.await;
let article = Article::test_article();
let req = test::TestRequest::with_uri(
format!("/delete-article/{}", ObjectId::new().to_hex()).as_str(),
)
.method(Method::DELETE)
.cookie(get_auth_cookie(
"wrong-cookie",
app_state.encryption.random_ascii_lc_string(32),
))
.set_payload(Bytes::from(serde_json::to_string(&article).unwrap()))
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_get_article() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.service(get_article),
)
.await;
let article = Article::test_article();
let (article_id, article_title) = insert_test_article(&app_state, article.clone())
.await
.unwrap();
let req = test::TestRequest::with_uri(format!("/article/{}", article_id.to_hex()).as_str())
.header("Accept", "application/json")
.method(Method::GET)
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let result: Article = test::read_body_json(resp).await;
assert_eq!(result.title, article_title);
let del_count = delete_test_article(&app_state, &article_id).await.unwrap();
assert_eq!(del_count, 1);
}
#[tokio::test]
async fn test_get_article_by_title() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.service(get_article_by_title),
)
.await;
let article = Article::test_article();
let (article_id, article_title) = insert_test_article(&app_state, article.clone())
.await
.unwrap();
let req = test::TestRequest::with_uri("/article-by-title")
.header("Accept", "application/json")
.method(Method::POST)
.set_form(&ArticleTitleFormData {
title: article_title.to_owned(),
})
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let result: Article = test::read_body_json(resp).await;
assert_eq!(result.title, article_title);
let del_count = delete_test_article(&app_state, &article_id).await.unwrap();
assert_eq!(del_count, 1);
}
#[tokio::test]
async fn test_get_articles_by_category() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.service(get_articles_by_category),
)
.await;
let article = Article::test_article();
let (article_id, article_title) = insert_test_article(&app_state, article.clone())
.await
.unwrap();
let req = test::TestRequest::with_uri("/articles/testing")
.header("Accept", "application/json")
.method(Method::GET)
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let results: Vec<Article> = test::read_body_json(resp).await;
let find_inserted = results.iter().find(|&art| art.title == article_title);
assert!(find_inserted.is_some());
let del_count = delete_test_article(&app_state, &article_id).await.unwrap();
assert_eq!(del_count, 1);
}
}
use crate::{
middleware::AuthenticatedAdminMiddleware, view_resource::ViewResourceManager, AppState,
};
use actix_web::{
get,
web::{Data, Path},
HttpRequest, Responder,
};
/// Returns the content of a ViewResource after retrieving it by name.
/// If the resource is not found (has not been registered), it returns the 404 ViewResource.
/// The regex matches uris with more than 3 characters so we don't match the /fr /es etc urls used for the websites translation directories
#[get("/{resource_name:.{3,}}")]
pub async fn get_view<'a>(
app_state: Data<AppState>,
resource_manager: Data<ViewResourceManager>,
auth_middleware: Data<AuthenticatedAdminMiddleware<'a>>,
req: HttpRequest,
resource_name: Path<String>,
) -> impl Responder {
resource_manager
.get_resource_as_http_response(
&app_state,
&auth_middleware,
&req,
None,
&resource_name.into_inner(),
)
.await
}
/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
* _______ ______ ______ _______ *@@
* |__ __@ | ____@ / ____@ |__ __@ *@@
* | @ | @__ \_ @_ | @ *@@
* | @ | __@ \ @_ | @ *@@
* | @ | @___ ____\ @ | @ *@@
* |__@ |______@ \______@ |__@ *@@
* *@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@
*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@*/
#[cfg(test)]
mod test_views {
use super::*;
use crate::{
middleware::get_auth_cookie,
model::{AdminAuthCredentials, Administrator},
view_resource::ViewResourceDescriptor,
};
use actix_web::{
http::StatusCode,
test,
web::{Bytes, Data},
App,
};
fn get_views_manager() -> ViewResourceManager {
ViewResourceManager::with_views(vec![
ViewResourceDescriptor {
path_str: "test-view",
index_file_name: "index.html",
resource_name: "test-view",
apply_auth_middleware: false,
},
ViewResourceDescriptor {
path_str: "test-view-auth",
index_file_name: "index.html",
resource_name: "test-view-auth",
apply_auth_middleware: true,
},
])
}
async fn get_authenticated_admin(app_state: &AppState) -> Administrator {
Administrator::authenticated(
app_state,
AdminAuthCredentials {
username: app_state.env.default_admin_username.to_owned(),
password: app_state.env.default_admin_password.to_owned(),
},
)
.await
.unwrap()
}
#[tokio::test]
async fn test_get_view() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.app_data(Data::new(get_views_manager()))
.service(get_view),
)
.await;
let req = test::TestRequest::with_uri("/test-view").to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = test::read_body(resp).await;
assert_eq!(body, Bytes::from("<h1>TEST</h1>"));
}
#[tokio::test]
async fn test_get_view_auth() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.app_data(Data::new(get_views_manager()))
.service(get_view),
)
.await;
let admin_user = get_authenticated_admin(&app_state).await;
let req = test::TestRequest::with_uri("/test-view-auth")
.cookie(get_auth_cookie(
"kuadrado-admin-auth",
app_state
.encryption
.decrypt(&admin_user.auth_token.unwrap())
.to_owned(),
))
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = test::read_body(resp).await;
assert_eq!(body, Bytes::from("<h1>TEST AUTH</h1>"));
}
#[tokio::test]
async fn test_get_view_auth_unauthorized() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.app_data(Data::new(get_views_manager()))
.service(get_view),
)
.await;
let req = test::TestRequest::with_uri("/test-view-auth").to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_get_view_not_found() {
dotenv::dotenv().ok();
let app_state = AppState::for_test().await;
let mut app = test::init_service(
App::new()
.app_data(Data::new(app_state.clone()))
.app_data(Data::new(AuthenticatedAdminMiddleware::new(
"kuadrado-admin-auth",
)))
.app_data(Data::new(get_views_manager()))
.service(get_view),
)
.await;
let req = test::TestRequest::with_uri("/whatever").to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
}
use crate::{
middleware::AuthenticatedAdminMiddleware,
model::AdminAuthCredentials,
// view_resource::{ViewResource, ViewResourceDescriptor},
AppState,
};
use actix_web::{web::Form, HttpMessage, HttpRequest, HttpResponse};
use std::{env::var as env_var, fs::read_to_string as file_to_string, path::PathBuf};
#[derive(Debug, Clone)]
/// Loads a static resource data allowing it to be served by the get_view service.
/// It holds a name, allowing the resource to be retrived by name,
/// a content which can be any text content stored in a string (like an html document),
/// a path to the directory of the actual static resource, and a boolean which indicates wether
/// or not an authentication verification should be applied.
pub struct ViewResource {
pub name: String,
pub string_contents: String,
pub dir_path: PathBuf,
pub apply_auth_middleware: bool,
}
#[derive(Debug, Clone)]
/// Defines the values that will be used to construct a ViewResource.
/// It must be passed to the AppViewResourceManager for resource registration
pub struct ViewResourceDescriptor<'a> {
pub path_str: &'a str,
pub index_file_name: &'a str,
pub resource_name: &'a str,
pub apply_auth_middleware: bool,
}
#[derive(Debug, Clone)]
/// A structure reponsible of registering and retrieving static resources.
pub struct ViewResourceManager {
resources: Vec<ViewResource>,
}
impl ViewResourceManager {
pub fn new() -> Self {
ViewResourceManager { resources: vec![] }
}
/// Calls the constructor and registers the resources described as argument before returning the instance
pub fn with_views(resource_descriptors: Vec<ViewResourceDescriptor>) -> Self {
let mut instance = Self::new();
instance.register_batch(resource_descriptors);
instance
}
/// Registers a new static resource in the instance.
/// The path provided in the argument must point to an existing file
pub fn register(&mut self, desc: ViewResourceDescriptor) {
let static_dir = std::path::PathBuf::from(
env_var("RESOURCES_DIR").expect("RESOURCES_DIR is not defined"),
)
.join("public/views");
let dir_path = static_dir.join(desc.path_str);
let path: PathBuf = format!("{}/{}", dir_path.to_str().unwrap(), desc.index_file_name)
.parse()
.expect(&format!(
"Failed to pare resource index file path {:?}",
desc.index_file_name
));
let string_contents = file_to_string(path).unwrap();
&self.resources.push(ViewResource {
name: desc.resource_name.to_string(),
dir_path,
string_contents,
apply_auth_middleware: desc.apply_auth_middleware,
});
}
/// Registers a collection of multiple resources.
pub fn register_batch(&mut self, resource_descriptors: Vec<ViewResourceDescriptor>) {
for desc in resource_descriptors.iter() {
self.register(desc.clone());
}
}
/// Retrieves a resource by name and returns a reference to it or None.
pub fn get_resource(&self, name: &str) -> Option<&ViewResource> {
self.resources.iter().find(|res| res.name == name)
}
/// Retrieves a resource by name and returns it as an http response.
/// This can be returned as it by a service.
pub async fn get_resource_as_http_response<'a>(
&self,
app_state: &AppState,
auth_middleware: &AuthenticatedAdminMiddleware<'a>,
req: &HttpRequest,
auth_data: Option<Form<AdminAuthCredentials>>,
resource_name: &str,
) -> HttpResponse {
match self.get_resource(resource_name) {
Some(res) => {
if res.apply_auth_middleware {
let auth_cookie = auth_middleware.exec(app_state, req, auth_data).await;
if auth_cookie.is_err() {
let unauthorized_view = match self.get_resource("unauthorized") {
Some(res_404) => res_404.string_contents.to_string(),
None => {
println!("WARNING: missing Unauthorized view resource");
"
<h1>Unauthorized</h1>
<p>You must login as an administrator to access this page</p>
<a href='/v/admin-login'>Login page</a>
"
.to_string()
}
};
let mut response_builder = HttpResponse::Unauthorized();
return match req.cookie(auth_middleware.cookie_name) {
Some(cookie) => {
// Invalidate auth_cookie if auth failed in any way
response_builder
.del_cookie(&cookie)
.content_type("text/html")
.body(unauthorized_view)
}
None => response_builder
.content_type("text/html")
.body(unauthorized_view),
};
} else {
return HttpResponse::Ok()
.content_type("text/html")
.cookie(auth_cookie.unwrap())
.body(&res.string_contents);
}
}
HttpResponse::Ok()
.content_type("text/html")
.body(&res.string_contents)
}
None => match self.get_resource("404") {
Some(res_404) => HttpResponse::NotFound()
.content_type("text/html")
.body(&res_404.string_contents),
None => {
println!("WARNING: missing 404 view resource");
HttpResponse::NotFound().finish()
}
},
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment