/* * Copyright (C) 2022 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 std::collections::HashMap; use actix_web::{web, HttpResponse, Responder}; use actix_web_httpauth::middleware::HttpAuthentication; use serde::{Deserialize, Serialize}; use crate::db::FormSubmission; use crate::errors::*; use crate::AppCtx; use crate::*; use super::bearerauth; pub mod routes { use super::*; #[derive(Debug, Eq, PartialEq, Deserialize, Serialize)] pub struct Forms { pub submit: &'static str, pub get_all: &'static str, } impl Forms { pub const fn new() -> Self { Self { submit: "/api/v1/forms/submit", get_all: "/api/v1/forms/list", } } pub fn get_submit(&self, host: &str, path: &str) -> String { format!("{}?host={}&path={}", self.submit, host, path) } } } pub fn services(cfg: &mut web::ServiceConfig) { cfg.service(upload); cfg.service(list_all); } #[derive(Deserialize, Serialize, Debug)] #[serde(untagged)] enum FormDType { Num(f64), Str(String), } impl FormDType { fn apply_types(&mut self) { if let Self::Str(data) = self { if let Ok(num) = data.parse::() { *self = Self::Num(num); } } } } pub type FormValue = HashMap; #[derive(Serialize, Deserialize, Clone, Debug)] struct Page { page: usize, } #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Table { pub host: String, pub path: String, } #[derive(Serialize, Deserialize, Clone, Debug, Default)] pub struct FormSubmissionResp { pub value: Option, pub time: i64, } impl From for FormSubmissionResp { fn from(f: FormSubmission) -> Self { Self { value: f.value, time: f.time.unix_timestamp(), } } } #[actix_web_codegen_const_routes::post( path = "API_V1_ROUTES.forms.get_all", wrap = "HttpAuthentication::bearer(bearerauth)" )] #[tracing::instrument(name = "Get form submissions", skip(ctx))] async fn list_all( ctx: AppCtx, payload: web::Json, page: web::Query, ) -> ServiceResult { let mut subs = ctx .db .get_form_submissions(page.page, &payload.host, &payload.path) .await?; let mut resp: Vec = Vec::with_capacity(subs.len()); for sub in subs.drain(0..) { resp.push(sub.into()); } Ok(HttpResponse::Ok().json(resp)) } #[actix_web_codegen_const_routes::post(path = "API_V1_ROUTES.forms.submit")] #[tracing::instrument(name = "Upload form", skip(ctx, payload))] async fn upload( ctx: AppCtx, query: web::Query
, payload: web::Either, web::Form>, ) -> ServiceResult { let host = &query.host; let path = &query.path; let data = match payload { web::Either::Left(json) => json.into_inner(), web::Either::Right(form) => { let mut form = form.into_inner(); for (_, v) in form.iter_mut() { v.apply_types(); } serde_json::to_value(&form).unwrap() } }; ctx.db .add_form_submission(&data, &host, path) .await .unwrap(); Ok(HttpResponse::Ok().json(data)) } #[cfg(test)] pub mod tests { use actix_web::{ http::{header, StatusCode}, test, App, }; use super::*; #[derive(Serialize, Clone, Debug, Deserialize, PartialEq)] struct Foo { foo: String, num: f64, } #[actix_rt::test] async fn submit_works() { // const USERNAME: &str = "index_works"; // const PASSWORD: &str = "23k4j;123k4j1;l23kj4"; let settings = Settings::new().unwrap(); // let settings = Settings::new().unwrap(); let ctx = AppCtx::new(crate::ctx::Ctx::new(&settings).await); let app = test::init_service( App::new() .app_data(ctx.clone()) .configure(crate::routes::services), ) .await; let host = "localhost:8008"; let path = "/foo"; let upload_path = API_V1_ROUTES.forms.get_submit(host, path); println!("{upload_path}"); let _ = ctx.db.delete_site(host).await; let foo = Foo { foo: "Foo".into(), num: 2.33, }; // upload json let upload_json = test::call_service( &app, test::TestRequest::post() .uri(&upload_path) .set_json(&foo) .to_request(), ) .await; if upload_json.status() != StatusCode::OK { let resp_err: crate::errors::ErrorToResponse = test::read_body_json(upload_json).await; panic!("{:?}", resp_err.error); } assert_eq!(upload_json.status(), StatusCode::OK); let json: serde_json::Value = test::read_body_json(upload_json).await; // upload url encoded let upload_form = test::call_service( &app, test::TestRequest::post() .uri(&upload_path) .set_form(&foo) .to_request(), ) .await; if upload_form.status() != StatusCode::OK { let resp_err: crate::errors::ErrorToResponse = test::read_body_json(upload_form).await; panic!("{:?}", resp_err.error); } assert_eq!(upload_form.status(), StatusCode::OK); let form: serde_json::Value = test::read_body_json(upload_form).await; assert_eq!(form, json); let get_sub_route = format!("{}?page={}", API_V1_ROUTES.forms.get_all, 0); let payload = Table { host: host.into(), path: path.to_owned(), }; println!("{get_sub_route}"); let get_subs = test::call_service( &app, test::TestRequest::post() .set_json(&payload) .uri(&get_sub_route) .insert_header(( header::AUTHORIZATION, format!("Bearer {}", ctx.settings.dash.api_key), )) .to_request(), ) .await; println!("{:?}", get_subs); assert_eq!(get_subs.status(), StatusCode::OK); let subs: Vec = test::read_body_json(get_subs).await; let foo_as_json_value = serde_json::to_value(&foo).unwrap(); assert_eq!(subs.len(), 2); assert_eq!(subs[0].value.as_ref().unwrap(), &foo_as_json_value); assert_eq!(subs[1].value.as_ref().unwrap(), &foo_as_json_value); } }