getting things to work a bit

This commit is contained in:
Chris Cochrun 2025-06-19 16:13:59 -05:00
parent 760e46a2d1
commit 6913426619
20 changed files with 4607 additions and 1880 deletions

3013
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -21,3 +21,20 @@ uuid = "1.6.1"
sanitize-filename = "0.5.0"
lettre = { version = "0.11.3", features = ["smtp-transport"] }
markup = "0.15.0"
maud = { version = "0.26.0", features = ["actix-web"] }
log4rs = "1.3.0"
actix-cors = "0.7.0"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["fmt", "std", "chrono", "time", "local-time", "env-filter"] }
tracing-appender = "0.2.3"
actix-files = "0.6.6"
tracing-actix-web = "0.7.14"
color-eyre = "0.6.3"
pretty_assertions = "1.4.1"
sqlx = { version = "0.8.2", features = ["sqlite"] }
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3

View file

@ -59,4 +59,4 @@ Our vision is to change the world from the heart of America by providing discipl
## Our Mission
We do that by, connecting teens with the truth of Jesus Christ…teaching and equipping them to live out the Great Commission.
{{ disciplemaking }}
{{ disciplemaking() }}

0
data.db Normal file
View file

View file

@ -1,7 +1,7 @@
default:
just --list
build:
zola build
tailwindcss -i static/css/base.css -o static/css/main.css && zola build
serve:
zola serve
uglify:

View file

@ -1,318 +1,360 @@
use std::collections::HashMap;
use actix_multipart::form::{text::Text, MultipartForm};
use actix_web::{post, HttpResponse};
use lettre::{
message::MultiPart,
transport::smtp::authentication::{Credentials, Mechanism},
Message, SmtpTransport, Transport,
};
use actix_web::{http::StatusCode, post, HttpResponse, HttpResponseBuilder};
use color_eyre::eyre::{Context, Result};
use futures::FutureExt;
use lettre::{message::SinglePart, Message};
use maud::{html, Markup, DOCTYPE};
use reqwest::Client;
use tracing::{error, info};
use super::errors::ApiError;
#[derive(Debug, MultipartForm, Default)]
#[derive(Debug, MultipartForm)]
struct CampForm {
#[multipart(rename = "first-name")]
first_name: Option<Text<String>>,
first_name: Text<String>,
#[multipart(rename = "last-name")]
last_name: Option<Text<String>>,
last_name: Text<String>,
#[multipart(rename = "parent-first-name")]
parent_first_name: Option<Text<String>>,
parent_first_name: Text<String>,
#[multipart(rename = "parent-last-name")]
parent_last_name: Option<Text<String>>,
birthdate: Option<Text<String>>,
gender: Option<Text<String>>,
street: Option<Text<String>>,
city: Option<Text<String>>,
state: Option<Text<String>>,
zip: Option<Text<i32>>,
parent_last_name: Text<String>,
#[multipart(rename = "birth-date")]
birthdate: Text<String>,
gender: Text<String>,
street: Text<String>,
city: Text<String>,
state: Text<String>,
zip: Text<i32>,
#[multipart(rename = "parent-phone")]
parent_phone: Option<Text<String>>,
parent_phone: Text<String>,
#[multipart(rename = "parent-email")]
parent_email: Option<Text<String>>,
grade: Option<Text<String>>,
shirt: Option<Text<String>>,
allergies: Option<Text<String>>,
week: Option<Text<String>>,
registration: Option<Text<String>>,
parent_email: Text<String>,
grade: Text<String>,
shirt: Text<String>,
allergies: Text<String>,
week: Text<String>,
registration: Text<String>,
#[multipart(rename = "health-form")]
health_form: Text<String>,
}
#[post("/camp-form")]
impl From<&CampForm> for HashMap<i32, String> {
fn from(form: &CampForm) -> Self {
let mut map = HashMap::new();
map.insert(63, format!("{} {}", form.first_name.0, form.last_name.0));
map.insert(
64,
format!("{} {}", form.parent_first_name.0, form.parent_last_name.0),
);
map.insert(65, form.parent_phone.0.clone());
map.insert(66, form.parent_email.0.clone().clone());
map.insert(67, form.birthdate.0.clone());
map.insert(69, form.gender.0.clone());
map.insert(70, form.street.0.clone());
map.insert(71, form.city.0.clone());
map.insert(72, form.state.0.clone());
map.insert(73, form.zip.0.clone().to_string());
map.insert(74, form.grade.0.clone());
map.insert(75, form.week.0.clone());
map.insert(76, form.shirt.0.clone());
map.insert(77, form.registration.0.clone());
map.insert(115, form.health_form.0.clone());
map
}
}
impl CampForm {
fn build_email(&self) -> Markup {
html! {
(DOCTYPE)
meta charset="utf-8";
html {
head {
title { (self.first_name.0) " " (self.last_name.0) " signed up for camp!" }
style {
"table { border-collapse: collapse; width: 100% }"
"td, th { padding: 8px }"
"td { text-align: left; width: 70%; word-wrap: break-word }"
"th { text-align: right; border-right: 1px solid #ddd }"
"tr { border-bottom: 1px solid #ddd }"
"h1 { text-align: center }"
}
}
body {
h1 { "Camp form for " (self.first_name.0) " " (self.last_name.0) "!" }
hr;
table {
tr {
th { "Name" }
td { (self.first_name.0) " " (self.last_name.0) }
}
tr {
th { "Parent" }
td { (self.parent_first_name.0) " " (self.parent_last_name.0) }
}
tr {
th { "Birthdate" }
td { (self.birthdate.0) }
}
tr {
th { "Gender" }
td { (self.gender.0) }
}
tr {
th { "Street" }
td { (self.street.0) }
}
tr {
th { "City" }
td { (self.city.0) }
}
tr {
th { "State" }
td { (self.state.0) }
}
tr {
th { "Zip" }
td { (self.zip.0) }
}
tr {
th { "Parent Phone" }
td { (self.parent_phone.0) }
}
tr {
th { "Parent Email" }
td { (self.parent_email.0) }
}
tr {
th { "Grade" }
td { (self.grade.0) }
}
tr {
th { "Camper Allergies" }
td { (self.allergies.0) }
}
tr {
th { "T-Shirt Size" }
td { (self.shirt.0) }
}
tr {
th { "Week Choice" }
td { (self.week.0) }
}
tr {
th { "Health Form" }
td { (self.health_form.0) }
}
tr {
th { "Registration" }
td { (self.registration.0) }
}
}
}
}
}
}
fn prepare_email(&self) -> Result<Message> {
let first = self.first_name.clone();
let last = self.last_name.clone();
let email_subject = format!("{} {} signed up for camp!", first, last);
info!("{first} {last} signed up for camp!");
let email = self.build_email();
// let temp_file = self.get_temp_file();
// let multi = if let Some((file, path, content_type)) = temp_file {
// let filebody = fs::read(path);
// let content_type =
// ContentType::parse(&content_type.unwrap_or(String::from("image/jpg"))).unwrap();
// let attachment = Attachment::new(file).body(filebody.unwrap(), content_type);
// // info!(?attachment);
// MultiPart::mixed()
// .singlepart(SinglePart::html(email.into_string()))
// .singlepart(attachment)
// } else {
// MultiPart::alternative_plain_html(String::from("Testing"), email.into_string())
// };
let singlepart = SinglePart::html(email.into_string());
Message::builder()
.from(
"TFC ADMIN <no-reply@mail.tfcconnection.org>"
.parse()
.unwrap(),
)
.to("Chris Cochrun <chris@tfcconnection.org>".parse().unwrap())
.to("Ethan Rose <ethan@tfcconnection.org>".parse().unwrap())
.subject(email_subject)
.singlepart(singlepart)
// .multipart(multi)
.wrap_err("problemss")
}
}
#[post("/api/camp-form")]
pub async fn camp_form(MultipartForm(form): MultipartForm<CampForm>) -> HttpResponse {
log::info!("a new form");
let first = form
.first_name
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let last = form
.last_name
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let full_name = format!("{} {}", first, last);
let email_subject = format!("{} {} signed up for camp!", first, last);
let parent = format!(
"{} {}",
form.parent_first_name
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone(),
form.parent_last_name
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone()
);
let birthdate = form
.birthdate
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let gender = form
.gender
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let street = form
.street
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let city = form
.city
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let state = form
.state
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let zip = form.zip.as_ref().unwrap_or(&Text(0)).0;
let parent_phone = form
.parent_phone
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let parent_email = form
.parent_email
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let grade = form
.grade
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let shirt = form
.shirt
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let allergies = form
.allergies
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let week = form
.week
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let registration = form
.registration
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let reg = registration.clone();
log::info!("{first} {last} signed up for camp!");
let email = markup::new! {
@markup::doctype()
html {
head {
title { @format!("{} {} signed up for camp!", first, last) }
style {
"table { border-collapse: collapse; width: 100% }"
"td, th { padding: 8px }"
"td { text-align: left; width: 70%; word-wrap: break-word }"
"th { text-align: right; border-right: 1px solid #ddd }"
"tr { border-bottom: 1px solid #ddd }"
"h1 { text-align: center }"
}
}
body {
h1 { @format!("Camp form for {} {}!", first, last) }
hr;
table {
tr {
th { "Name" }
td { @format!("{} {}", first, last) }
}
tr {
th { "Parent" }
td { @parent }
}
tr {
th { "Birthdate" }
td { @birthdate }
}
tr {
th { "Gender" }
td { @gender }
}
tr {
th { "Street" }
td { @street }
}
tr {
th { "City" }
td { @city }
}
tr {
th { "State" }
td { @state }
}
tr {
th { "Zip" }
td { @zip }
}
tr {
th { "Parent Phone" }
td { @parent_phone }
}
tr {
th { "Parent Email" }
td { @parent_email }
}
tr {
th { "Grade" }
td { @grade }
}
tr {
th { "Camper Allergies" }
td { @allergies }
}
tr {
th { "T-Shirt Size" }
td { @shirt }
}
tr {
th { "Week Choice" }
td { @week }
}
tr {
th { "Registration" }
td { @registration }
}
}
}
}
let full_name = format!("{} {}", form.first_name.0, form.last_name.0);
let map = (&form).into();
let future = store_camp_form(map);
actix_rt::spawn(future.map(|s| match s {
Ok(_) => info!("Successfully posted to nextcloud tables"),
Err(e) => log::error!("Error in posting camp data: {:?}", e),
}));
let email = form.prepare_email();
match email {
Ok(m) => {
let sent = crate::email::send_email(m);
actix_rt::spawn(sent.map(|s| match s {
Ok(_) => info!("Successfully sent form to email!"),
Err(e) => error!("There was an erroring sending form to email: {e}"),
}));
}
Err(e) => error!("error sending email {e}"),
};
let multi = MultiPart::alternative_plain_html(
String::from("A camp form was filled out!"),
email.to_string(),
);
if let Ok(m) = Message::builder()
.from(
"TFC ADMIN <no-reply@mail.tfcconnection.org>"
.parse()
.unwrap(),
)
.to("Chris Cochrun <chris@tfcconnection.org>".parse().unwrap())
// .to("Ethan Rose <ethan@tfcconnection.org>".parse().unwrap())
.subject(email_subject)
.multipart(multi)
{
let sender = SmtpTransport::relay("mail.tfcconnection.org")
.ok()
.unwrap()
.credentials(Credentials::new(
"no-reply@mail.tfcconnection.org".to_owned(),
"r9f36mNZFtiW4f".to_owned(),
))
.authentication(vec![Mechanism::Plain])
.build();
match sender.send(&m) {
Ok(res) => log::info!("{:?}", res),
Err(e) => log::error!("{e}"),
}
} else {
log::info!("Email incorrect");
}
match reg.as_str() {
match form.health_form.0.as_str() {
"now" => {
log::info!("Sending them to pay for registration now");
info!("Sending them to fill out the health form");
HttpResponse::Ok()
.insert_header(("Access-Control-Expose-Headers", "*"))
.insert_header((
"HX-Redirect",
"https://secure.myvanco.com/L-Z772/campaign/C-13JPJ",
format!(
"/camp-health-form/?registration={}",
form.registration.0.as_str()
),
))
.finish()
}
"full" => {
log::info!("Sending them to pay for the full registration now");
HttpResponse::Ok()
.insert_header(("Access-Control-Expose-Headers", "*"))
.insert_header((
"HX-Redirect",
"https://secure.myvanco.com/L-Z772/campaign/C-13JQE",
))
.finish()
}
"later" => {
log::info!("{} would like to pay later", full_name);
let html = markup::new! {
div {
class { "mt-8" }
h2 {
@format!("Thank you, {}!", full_name)
"later" => match form.registration.0.as_str() {
"now" => {
info!("Sending them to pay for registration now");
HttpResponse::Ok()
.insert_header(("Access-Control-Expose-Headers", "*"))
.insert_header((
"HX-Redirect",
"https://secure.myvanco.com/L-Z772/campaign/C-13JPJ",
))
.finish()
}
"full" => {
info!("Sending them to pay for the full registration now");
HttpResponse::Ok()
.insert_header(("Access-Control-Expose-Headers", "*"))
.insert_header((
"HX-Redirect",
"https://secure.myvanco.com/L-Z772/campaign/C-13JQE",
))
.finish()
}
"later" => {
info!("{} would like to pay later", full_name);
let html = html! {
div class="mt-8" {
h2 {
"Thank you, " (full_name) "!"
}
p { "Can't wait to see you at camp!" }
p {
class { "" }
"If you'd like to pay for your registration go to the donate tab in the top right when you are ready and find the camp registration option."
}
}
p { "Can't wait to see you at camp!" }
p {
class { "" }
"If you'd like to pay for your registration go to the donate tab in the top right when you are ready and find the camp registration option."
};
HttpResponse::Ok().body(html.into_string())
}
_ => {
log::error!("Got registration error.....");
let html = html! {
div class="mt-8" {
h2 {
"Thank you, " (full_name) "!"
}
p { "Can't wait to see you at camp!" }
p {
class { "" }
"If you'd like to pay for your registration go to the donate tab in the top right when you are ready and find the camp registration option."
}
}
}
};
HttpResponse::Ok().body(html.to_string())
}
};
HttpResponse::Ok().body(html.into_string())
}
},
_ => {
log::error!("Got registration error.....");
let html = markup::new! {
div {
class { "mt-8" }
h2 {
@format!("Thank you, {}!", full_name)
}
p { "Can't wait to see you at camp!" }
p {
class { "" }
"If you'd like to pay for your registration go to the donate tab in the top right when you are ready and find the camp registration option."
}
}
};
HttpResponse::Ok().body(html.to_string())
log::error!("Unknown selection for health. We don't know where to send the user.");
HttpResponseBuilder::new(StatusCode::IM_A_TEAPOT)
.body("Unknown selection for health. We don't know where to send the user.")
}
}
}
async fn store_camp_form(form: CampForm) -> Result<(), ApiError> {
async fn store_camp_form(map: HashMap<i32, String>) -> Result<()> {
let request = Client::new();
let mut json = HashMap::new();
json.insert("data", map);
request
.post("https://staff.tfcconnection.org/apps/tables/api/1/tables/5/rows")
.basic_auth("chris", Some("2VHeGxeC^Zf9KqFK^G@Pt!zu2q^6@b"))
.header("OCS-APIRequest", "true")
.header("Content-Type", "application/json")
.json(&json)
.send()
.await?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use actix_web::test;
fn form() -> CampForm {
CampForm {
first_name: Text("Frodo".into()),
last_name: Text("Braggins".into()),
parent_first_name: Text("Bilbo".into()),
parent_last_name: Text("Braggins".into()),
birthdate: Text(String::from("1845-09-12")),
gender: Text(String::from("male")),
street: Text(String::from("1234 Bag End")),
city: Text(String::from("The Shire")),
state: Text(String::from("Hobbiton")),
zip: Text(88888),
parent_phone: Text(String::from("1234567898")),
parent_email: Text(String::from("bilbo@hobbits.com")),
grade: Text(String::from("junior")),
shirt: Text(String::from("medium")),
allergies: Text(String::from("Cool beans")),
week: Text(String::from("1")),
registration: Text(String::from("later")),
health_form: Text(String::from("I guess")),
}
}
#[test]
async fn test_nc_post() {
let form = form();
assert!(!form.first_name.is_empty());
let map = HashMap::from(&form);
let res = store_camp_form(map).await;
match res {
Ok(_) => assert!(true),
Err(e) => assert!(false, "Failed storing test: {e}"),
}
}
#[test]
async fn test_email() {
let form = form();
assert!(!form.first_name.is_empty());
match form.prepare_email() {
Ok(m) => {
assert!(crate::email::send_email(m).await.is_ok())
}
Err(e) => assert!(false, "Failed emailing test: {e}"),
}
}
}

168
src/api/contact.rs Normal file
View file

@ -0,0 +1,168 @@
use std::{
collections::{BTreeMap, HashMap},
fs,
};
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
use actix_web::{post, web, HttpResponse};
use color_eyre::{eyre::eyre, Result};
use lettre::{
message::{header::ContentType, Attachment, MultiPart, SinglePart},
Message,
};
use maud::{html, Markup, PreEscaped, DOCTYPE};
use reqwest::Client;
use serde_json::json;
use sqlx::SqliteConnection;
use tracing::{error, info};
#[derive(Debug, MultipartForm)]
struct ContactForm {
name: Text<String>,
email: Text<String>,
staff: Text<String>,
message: Text<String>,
}
impl From<&ContactForm> for HashMap<i32, String> {
fn from(form: &ContactForm) -> Self {
let mut map = HashMap::new();
map.insert(169, form.name.0.clone());
map.insert(169, form.email.0.clone());
map.insert(169, form.staff.0.clone());
map.insert(169, form.message.0.clone());
map
}
}
impl ContactForm {
async fn build_email(&self) -> Markup {
html! {
(DOCTYPE)
meta charset="utf-8";
html {
head {
title { (self.name.0) " filled out a contact form!" }
style {
"table { border-collapse: collapse; width: 100% }"
"td, th { padding: 8px }"
"td { text-align: left; width: 70%; word-wrap: break-word }"
"th { text-align: right; border-right: 1px solid #ddd }"
"tr { border-bottom: 1px solid #ddd }"
"h1 { text-align: center }"
}
}
body {
h1 { "Contact form for " (self.name.0) "!" }
hr;
p { "Email: " (self.email.0) }
p { "To: " (self.staff.0) }
hr;
p { (self.message.0) }
}
}
}
}
async fn store_form(&self) -> Result<()> {
let client = Client::new();
let map = HashMap::from(self);
let mut json = HashMap::new();
json.insert("data", map);
let res = client
.post("https://staff.tfcconnection.org/ocs/v2.php/apps/tables/api/2/tables/140/rows")
.basic_auth("chris", Some("2VHeGxeC^Zf9KqFK^G@Pt!zu2q^6@b"))
.header("OCS-APIRequest", "true")
.header("Content-Type", "application/json")
.json(&json)
.send()
.await?;
if res.status().is_success() {
let res = res.text().await.unwrap();
Ok(())
} else {
Err(eyre!(
"Problem in storing data: {:?}",
res.error_for_status()
))
}
}
async fn send_email(&mut self) -> Result<()> {
let name = self.name.clone();
let email_subject = format!("Contact form for {}!", name);
info!("{name} contact form!");
let email = self.build_email().await;
let email = SinglePart::html(email.into_string());
if let Ok(m) = Message::builder()
.from(
"TFC ADMIN <no-reply@mail.tfcconnection.org>"
.parse()
.unwrap(),
)
.to("Chris Cochrun <chris@tfcconnection.org>".parse().unwrap())
.to("Ethan Rose <ethan@tfcconnection.org>".parse().unwrap())
.subject(email_subject)
.singlepart(email)
{
crate::email::send_email(m).await
} else {
Err(eyre!("Email incorrect"))
}
}
}
#[post("/api/contact-form")]
pub async fn contact_form(MultipartForm(mut form): MultipartForm<ContactForm>) -> HttpResponse {
// match form.store_form().await {
// Ok(_) => info!("Successfully sent form to nextcloud!"),
// Err(e) => error!("There was an erroring sending form to nextcloud: {e}"),
// }
match form.send_email().await {
Ok(_) => info!("Successfully sent email"),
Err(e) => error!("There was an error sending the email: {e}"),
}
HttpResponse::Ok().body("Thank you! We will get back with you shortly!")
}
#[cfg(test)]
mod test {
use actix_web::test;
use pretty_assertions::assert_eq;
use sqlx::Connection;
use tracing::debug;
use super::*;
fn form() -> ContactForm {
ContactForm {
name: Text(String::from("Bilbo Braggins")),
email: Text(String::from("biblo@hobbits.us")),
staff: Text(String::from("Uncle")),
message: Text(String::from("Very")),
}
}
#[test]
async fn test_nc_post() {
let form = form();
assert!(!form.name.is_empty());
let res = form.store_form().await;
match res {
Ok(_) => assert!(true, "passed storing test"),
Err(e) => assert!(false, "Failed storing test: {e}"),
}
}
#[test]
async fn test_email() {
let mut form = form();
assert!(!form.name.is_empty());
match form.send_email().await {
Ok(_) => assert!(true, "passed emailing test"),
Err(e) => assert!(false, "Failed emailing test: {e}"),
}
}
}

View file

@ -18,8 +18,3 @@ pub struct ApiError {
pub cause: Option<String>,
pub error_type: ApiErrorType,
}
#[derive(Serialize)]
pub struct ApiErrorResponse {
pub error: String,
}

View file

@ -1,454 +1,381 @@
use std::fs;
use std::{collections::HashMap, fs};
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
use actix_web::{post, HttpResponse};
use color_eyre::{
eyre::{eyre, Context},
Result,
};
use futures::FutureExt;
use lettre::{
message::{header::ContentType, Attachment, MultiPart, SinglePart},
transport::smtp::authentication::{Credentials, Mechanism},
Message, SmtpTransport, Transport,
Message,
};
use maud::{html, Markup, DOCTYPE};
use reqwest::Client;
use tracing::{error, info};
#[derive(Debug, MultipartForm, Default)]
use crate::email::send_email;
#[derive(Debug, MultipartForm)]
struct HealthForm {
#[multipart(rename = "first-name")]
first_name: Option<Text<String>>,
first_name: Text<String>,
#[multipart(rename = "last-name")]
last_name: Option<Text<String>>,
last_name: Text<String>,
#[multipart(rename = "parent-first-name")]
parent_first_name: Option<Text<String>>,
parent_first_name: Text<String>,
#[multipart(rename = "parent-last-name")]
parent_last_name: Option<Text<String>>,
birthdate: Option<Text<String>>,
street: Option<Text<String>>,
city: Option<Text<String>>,
state: Option<Text<String>>,
zip: Option<Text<String>>,
parent_last_name: Text<String>,
#[multipart(rename = "birth-date")]
birthdate: Text<String>,
street: Text<String>,
city: Text<String>,
state: Text<String>,
zip: Text<String>,
#[multipart(rename = "cell-phone")]
parent_cellphone: Option<Text<String>>,
homephone: Option<Text<String>>,
parent_cellphone: Text<String>,
#[multipart(rename = "home-phone")]
homephone: Text<String>,
#[multipart(rename = "additional-emergency-contact")]
contact: Option<Text<String>>,
#[multipart(rename = "addtional-emergency-contact-phone")]
contact_phone: Option<Text<String>>,
doctorname: Option<Text<String>>,
doctorcity: Option<Text<String>>,
doctorphone: Option<Text<String>>,
contact: Text<String>,
#[multipart(rename = "additional-emergency-contact-phone")]
contact_phone: Text<String>,
#[multipart(rename = "doctor-name")]
doctorname: Text<String>,
#[multipart(rename = "doctor-city")]
doctorcity: Text<String>,
#[multipart(rename = "doctor-phone")]
doctorphone: Text<String>,
#[multipart(rename = "medical-coverage")]
medical: Option<Text<String>>,
medical: Text<String>,
#[multipart(rename = "insurance-name")]
insurance: Option<Text<String>>,
insurance: Text<String>,
#[multipart(rename = "policy-number")]
policy_number: Option<Text<String>>,
allergies: Option<Text<String>>,
policy_number: Text<String>,
allergies: Text<String>,
#[multipart(rename = "allergies-other")]
allergies_other: Option<Text<String>>,
allergies_other: Text<String>,
#[multipart(rename = "specific-allergies")]
specific_allergies: Option<Text<String>>,
specific_allergies: Text<String>,
#[multipart(rename = "allergic-treatment")]
treatment: Option<Text<String>>,
conditions: Option<Text<String>>,
treatment: Text<String>,
conditions: Text<String>,
#[multipart(rename = "tetanus-shot")]
tetanus: Option<Text<String>>,
tetanus: Text<String>,
#[multipart(rename = "swimming-ability")]
swimming: Option<Text<String>>,
swimming: Text<String>,
#[multipart(rename = "medication-schedule")]
medication: Option<Text<String>>,
medication: Text<String>,
#[multipart(rename = "other-notes")]
notes: Option<Text<String>>,
agreement: Option<Text<String>>,
notes: Text<String>,
agreement: Text<String>,
#[multipart(rename = "image")]
file: Option<TempFile>,
registration: Option<Text<String>>,
registration: Text<String>,
}
#[post("/health-form")]
pub async fn health_form(MultipartForm(form): MultipartForm<HealthForm>) -> HttpResponse {
let first = form
.first_name
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let last = form
.last_name
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let full_name = format!("{} {}", first, last);
let registration = form
.registration
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let email_subject = format!("{} {} filled out a health form!", first, last);
let filename_noext = format!("{}_{}", first, last);
let parent = format!(
"{} {}",
form.parent_first_name
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone(),
form.parent_last_name
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone()
);
let birthdate = form
.birthdate
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let street = form
.street
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let city = form
.city
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let state = form
.state
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let zip = form
.zip
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let parent_cellphone = form
.parent_cellphone
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let homephone = form
.homephone
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let contact = form
.contact
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let contact_phone = form
.contact_phone
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let doctorname = form
.doctorname
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let doctorcity = form
.doctorcity
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let doctorphone = form
.doctorphone
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let medical = form
.medical
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let insurance = form
.insurance
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let policy_number = form
.policy_number
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let agreement = form
.agreement
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let allergies = form
.allergies
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let allergies_other = form
.allergies_other
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let specific_allergies = form
.specific_allergies
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let treatment = form
.treatment
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let conditions = form
.conditions
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let tetanus = form
.tetanus
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let swimming = form
.swimming
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let medication = form
.medication
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let notes = form
.notes
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
log::info!("{first} {last} filled out a health form!");
let email = markup::new! {
@markup::doctype()
html {
head {
title { @format!("{} {} filled out a health form!", first, last) }
style {
"table { border-collapse: collapse; width: 100% }"
"td, th { padding: 8px }"
"td { text-align: left; width: 70%; word-wrap: break-word }"
"th { text-align: right; border-right: 1px solid #ddd }"
"tr { border-bottom: 1px solid #ddd }"
"h1 { text-align: center }"
impl From<&HealthForm> for HashMap<i32, String> {
fn from(form: &HealthForm) -> Self {
let mut map = HashMap::new();
map.insert(37, format!("{} {}", form.first_name.0, form.last_name.0));
map.insert(
38,
format!("{} {}", form.parent_first_name.0, form.parent_last_name.0),
);
map.insert(39, form.birthdate.0.clone());
map.insert(40, form.street.0.clone());
map.insert(41, form.city.0.clone());
map.insert(42, form.state.0.clone());
map.insert(43, form.zip.0.clone());
map.insert(44, form.parent_cellphone.0.clone());
map.insert(45, form.homephone.0.clone());
map.insert(46, format!("{} {}", form.contact.0, form.contact_phone.0));
map.insert(47, form.doctorname.0.clone());
map.insert(48, form.doctorcity.0.clone());
map.insert(49, form.doctorphone.0.clone());
map.insert(50, form.medical.0.clone());
map.insert(51, form.insurance.0.clone());
map.insert(52, form.policy_number.0.clone());
map.insert(54, form.agreement.0.clone());
map.insert(
55,
format!("{} \n {}", form.allergies.0, form.allergies_other.0),
);
map.insert(56, form.specific_allergies.0.clone());
map.insert(57, form.treatment.0.clone());
map.insert(58, form.conditions.0.clone());
map.insert(59, form.tetanus.0.clone());
map.insert(60, form.medication.0.clone());
map.insert(61, form.notes.0.clone());
map.insert(62, form.swimming.0.clone());
map
}
}
impl HealthForm {
fn build_email(&self) -> Markup {
html! {
(DOCTYPE)
meta charset="utf-8";
html {
head {
title { (self.first_name.0) " " (self.last_name.0) " filled out a health form!" }
style {
"table { border-collapse: collapse; width: 100% }"
"td, th { padding: 8px }"
"td { text-align: left; width: 70%; word-wrap: break-word }"
"th { text-align: right; border-right: 1px solid #ddd }"
"tr { border-bottom: 1px solid #ddd }"
"h1 { text-align: center }"
}
}
}
body {
h1 { @format!("Health form for {} {}!", first, last) }
hr;
table {
tr {
th { "Name" }
td { @format!("{} {}", first, last) }
}
tr {
th { "Parent" }
td { @parent }
}
tr {
th { "Birthdate" }
td { @birthdate }
}
tr {
th { "Street" }
td { @street }
}
tr {
th { "City" }
td { @city }
}
tr {
th { "State" }
td { @state }
}
tr {
th { "Zip" }
td { @zip }
}
tr {
th { "Phone" }
td { @parent_cellphone }
}
tr {
th { "Home Phone" }
td { @homephone }
}
tr {
th { "Additional Emergency Contact" }
td { @contact }
}
tr {
th { "Contact Phone" }
td { @contact_phone }
}
tr {
th { "Doctor" }
td { @doctorname }
}
tr {
th { "Doctor City" }
td { @doctorcity }
}
tr {
th { "Doctor Phone" }
td { @doctorphone }
}
tr {
th { "Medical Coverage" }
td { @medical }
}
tr {
th { "Insurance Provider" }
td { @insurance }
}
tr {
th { "Policy Number" }
td { @policy_number }
}
tr {
th { "Allergies" }
td { @allergies }
}
tr {
th { "Other Allergies" }
td { @allergies_other }
}
tr {
th { "Specific Allergies" }
td { @specific_allergies }
}
tr {
th { "Treatments" }
td { @treatment }
}
tr {
th { "Physical or mental conditions" }
td { @conditions }
}
tr {
th { "Last tetanus shot" }
td { @tetanus }
}
tr {
th { "Swimming Ability" }
td { @swimming }
}
tr {
th { "Medication Schedule" }
td { @medication }
}
tr {
th { "Other Relevant Info" }
td { @notes }
}
tr {
th { "Final Agreement" }
td { @agreement }
body {
h1 { "Health form for " (self.first_name.0) " " (self.last_name.0) "!" }
hr;
table {
tr {
th { "Name" }
td { (self.first_name.0) " " (self.last_name.0) }
}
tr {
th { "Parent" }
td { (self.parent_first_name.0) " " (self.parent_last_name.0) }
}
tr {
th { "Birthdate" }
td { (self.birthdate.0) }
}
tr {
th { "Street" }
td { (self.street.0) }
}
tr {
th { "City" }
td { (self.city.0) }
}
tr {
th { "State" }
td { (self.state.0) }
}
tr {
th { "Zip" }
td { (self.zip.0) }
}
tr {
th { "Parent Cell Phone" }
td { (self.parent_cellphone.0) }
}
tr {
th { "Homephone" }
td { (self.homephone.0) }
}
tr {
th { "Additional Emergency Contact" }
td { (self.contact.0) }
}
tr {
th { "Emegency Contact Phone" }
td { (self.contact_phone.0) }
}
tr {
th { "Doctor" }
td { (self.doctorname.0) }
}
tr {
th { "Doctor City" }
td { (self.doctorcity.0) }
}
tr {
th { "Doctor Phone" }
td { (self.doctorphone.0) }
}
tr {
th { "Medical Coverage" }
td { (self.medical.0) }
}
tr {
th { "Insurance Provider" }
td { (self.insurance.0) }
}
tr {
th { "Policy Number" }
td { (self.policy_number.0) }
}
tr {
th { "Allergies" }
td { (self.allergies.0)
"\n\n"
(self.allergies_other.0)
}
}
tr {
th { "Specific Allergies" }
td { (self.specific_allergies.0) }
}
tr {
th { "Allergic Treatments" }
td { (self.treatment.0) }
}
tr {
th { "Conditions" }
td { (self.conditions.0) }
}
tr {
th { "Date of last Tetanus Shot" }
td { (self.tetanus.0) }
}
tr {
th { "Swimming Ability" }
td { (self.swimming.0) }
}
tr {
th { "Medication Schedule" }
td { (self.medication.0) }
}
tr {
th { "Other Notes" }
td { (self.notes.0) }
}
tr {
th { "Final Agreement" }
td { (self.agreement.0) }
}
tr {
th { "Registration" }
td { (self.registration.0) }
}
}
}
}
}
};
let mut path: Option<String> = Some(String::from(""));
let mut file_exists = false;
let mut filename = String::from("");
log::info!("{:?}", file_exists);
if let Some(f) = form.file {
if let Some(file) = f.file_name {
if let Some(ext) = file.rsplit('.').next() {
filename = format!("{}.{}", filename_noext, ext);
path = Some(format!("./tmp/{}.{}", filename_noext, ext));
} else {
path = Some(format!("./tmp/{}", file));
}
// let path = format!("./tmp/{}", file);
log::info!("saving to {}", path.clone().unwrap());
match f.file.persist(path.clone().unwrap()) {
Ok(f) => {
log::info!("{:?}", f);
if f.metadata().unwrap().len() > 0 {
file_exists = true;
}
}
Err(e) => log::info!("{:?}: Probably a missing image", e),
}
}
}
let multi = if file_exists {
let filebody = fs::read(path.clone().unwrap_or_default());
let content_type = ContentType::parse("image/jpg").unwrap();
let attachment = Attachment::new(filename).body(filebody.unwrap(), content_type);
log::info!("{:?}", attachment);
MultiPart::mixed()
.singlepart(SinglePart::html(email.to_string()))
.singlepart(attachment)
} else {
MultiPart::alternative_plain_html(String::from("Testing"), email.to_string())
};
async fn store_form(&self) -> Result<()> {
let client = Client::new();
let map = HashMap::from(self);
let mut json = HashMap::new();
json.insert("data", map);
log::info!("{:?}", multi);
if let Ok(m) = Message::builder()
.from(
"TFC ADMIN <no-reply@mail.tfcconnection.org>"
.parse()
.unwrap(),
)
.to("Chris Cochrun <chris@tfcconnection.org>".parse().unwrap())
// .to("Ethan Rose <ethan@tfcconnection.org>".parse().unwrap())
.subject(email_subject)
.multipart(multi)
{
let sender = SmtpTransport::relay("mail.tfcconnection.org")
.ok()
.unwrap()
.credentials(Credentials::new(
"no-reply@mail.tfcconnection.org".to_owned(),
"r9f36mNZFtiW4f".to_owned(),
let link = r#"https://staff.tfcconnection.org/apps/tables/#/table/4/row/757"#;
let res = client
.post("https://staff.tfcconnection.org/ocs/v2.php/apps/tables/api/2/tables/4/rows")
.basic_auth("chris", Some("2VHeGxeC^Zf9KqFK^G@Pt!zu2q^6@b"))
.header("OCS-APIRequest", "true")
.header("Content-Type", "application/json")
.json(&json)
.send()
.await?;
if res.status().is_success() {
let res = res.text().await.unwrap();
Ok(())
} else {
Err(eyre!(
"Problem in storing data: {:?}",
res.error_for_status()
))
.authentication(vec![Mechanism::Plain])
.build();
match sender.send(&m) {
Ok(res) => log::info!("{:?}", res),
Err(e) => log::info!("{e}"),
}
} else {
log::info!("Email incorrect");
}
match registration.as_str() {
fn get_temp_file(&mut self) -> Option<(String, String, Option<String>)> {
let first = self.first_name.clone();
let last = self.last_name.clone();
let filename_noext = format!("{}_{}", first, last);
let (file_name, content_type) = if let Some(file) = self.file.as_ref() {
let content_type = file.content_type.clone().map(|m| m.to_string());
(file.file_name.to_owned(), content_type)
} else {
return None;
};
let filename;
let path = if let Some(file_name) = file_name {
if let Some(ext) = file_name.rsplit('.').next() {
filename = format!("{}.{}", filename_noext, ext);
format!("./tmp/{}.{}", filename_noext, ext)
} else {
filename = String::default();
format!("./tmp/{}", file_name)
}
} else {
filename = String::default();
String::default()
};
let file = self.file.take();
match file.unwrap().file.persist(path.clone()) {
Ok(f) => {
if f.metadata().unwrap().len() <= 0 {
return None;
}
info!(?f, "File saved successfully");
Some((filename, path, content_type.clone()))
}
Err(e) => {
error!("{:?}: Probably a missing image", e);
None
}
}
}
fn send_email(&mut self) -> Result<Message> {
let first = self.first_name.clone();
let last = self.last_name.clone();
let email_subject = format!("{} {} filled out a health form!", first, last);
info!("{first} {last} filled out a health form!");
let email = self.build_email();
let temp_file = self.get_temp_file();
let multi = if let Some((file, path, content_type)) = temp_file {
let filebody = fs::read(path);
let content_type =
ContentType::parse(&content_type.unwrap_or(String::from("image/jpg"))).unwrap();
let attachment = Attachment::new(file).body(filebody.unwrap(), content_type);
// info!(?attachment);
MultiPart::mixed()
.singlepart(SinglePart::html(email.into_string()))
.singlepart(attachment)
} else {
MultiPart::alternative_plain_html(String::from("Testing"), email.into_string())
};
Message::builder()
.from(
"TFC ADMIN <no-reply@mail.tfcconnection.org>"
.parse()
.unwrap(),
)
.to("Chris Cochrun <chris@tfcconnection.org>".parse().unwrap())
.to("Ethan Rose <ethan@tfcconnection.org>".parse().unwrap())
.subject(email_subject)
.multipart(multi)
.wrap_err("Email incorrect")
}
}
#[post("/api/health-form")]
pub async fn health_form(MultipartForm(mut form): MultipartForm<HealthForm>) -> HttpResponse {
info!("Starting health form work: {:?}", form);
match form.send_email() {
Ok(m) => {
actix_rt::spawn(send_email(m).map(|r| match r {
Ok(_) => info!("Email sent successfully"),
Err(e) => error!("There was an erroring sending form to nextcloud: {e}"),
}));
info!("Successfully sent email health form")
}
Err(e) => error!("There was an error sending email: {e}"),
}
let map = HashMap::from(&form);
actix_rt::spawn(store_form(map).map(|r| match r {
Ok(_) => {
info!("Successfully stored health form in nextcloud!")
}
Err(e) => error!("There was an error storing form in nextcloud: {e}"),
}));
let full_name = format!("{} {}", form.first_name.0, form.last_name.0);
match form.registration.0.as_str() {
"now" => {
log::info!("Sending them to pay for registration now");
info!("Sending them to pay for registration now");
HttpResponse::Ok()
.insert_header(("Access-Control-Expose-Headers", "*"))
.insert_header((
@ -458,7 +385,7 @@ pub async fn health_form(MultipartForm(form): MultipartForm<HealthForm>) -> Http
.finish()
}
"full" => {
log::info!("Sending them to pay for the full registration now");
info!("Sending them to pay for the full registration now");
HttpResponse::Ok()
.insert_header(("Access-Control-Expose-Headers", "*"))
.insert_header((
@ -468,43 +395,62 @@ pub async fn health_form(MultipartForm(form): MultipartForm<HealthForm>) -> Http
.finish()
}
"later" => {
log::info!("{} would like to pay later", full_name);
let html = markup::new! {
div {
class { "mt-8" }
info!("{} would like to pay later", full_name);
let html = html! {
div class="mt-8" {
h2 {
@format!("Thank you, {}!", full_name)
"Thank you, " (full_name) "!"
}
p { "Can't wait to see you at camp!" }
p {
class { "" }
"If you'd like to pay for your registration go to the donate tab in the top right when you are ready and find the camp registration option."
"If you'd like to pay for your registration go to the donate tab in the top right when you are ready and find the right registration option."
}
}
};
HttpResponse::Ok().body(html.to_string())
HttpResponse::Ok().body(html.into_string())
}
_ => {
log::error!("Got registration error.....");
let html = markup::new! {
div {
class { "mt-8" }
log::warn!(
"Got registration error possibly. Here is what the registration was: {}",
form.registration.0.as_str()
);
let html = html! {
div class="mt-8" {
h2 {
@format!("Thank you, {}!", full_name)
"Thank you, " (full_name) "!"
}
p { "Can't wait to see you at camp!" }
p {
class { "" }
"If you'd like to pay for your registration go to the donate tab in the top right when you are ready and find the camp registration option."
"If you filled this out for camp or mission trip you can pay for your registration at the donate tab in the top right when you are ready and find the camp or mission trip registration option."
}
}
};
HttpResponse::Ok().body(html.to_string())
HttpResponse::Ok().body(html.into_string())
}
}
// HttpResponse::Ok().body("hi")
}
async fn store_health_form(_form: HealthForm) -> bool {
todo!()
async fn store_form(map: HashMap<i32, String>) -> Result<()> {
let client = Client::new();
let mut json = HashMap::new();
json.insert("data", map);
let res = client
.post("https://staff.tfcconnection.org/ocs/v2.php/apps/tables/api/2/tables/4/rows")
.basic_auth("chris", Some("2VHeGxeC^Zf9KqFK^G@Pt!zu2q^6@b"))
.header("OCS-APIRequest", "true")
.header("Content-Type", "application/json")
.json(&json)
.send()
.await?;
if res.status().is_success() {
let res = res.text().await.unwrap();
Ok(())
} else {
Err(eyre!(
"Problem in storing data: {:?}",
res.error_for_status()
))
}
}

View file

@ -1,10 +1,9 @@
use actix_multipart::form::{text::Text, MultipartForm};
use actix_web::{post, HttpResponse};
use lettre::{
message::MultiPart,
transport::smtp::authentication::{Credentials, Mechanism},
Message, SmtpTransport, Transport,
};
use lettre::{message::MultiPart, Message};
use tracing::info;
use crate::email::send_email;
#[derive(Debug, MultipartForm, Default)]
struct LocalForm {
@ -165,7 +164,7 @@ pub async fn local_form(MultipartForm(form): MultipartForm<LocalForm>) -> HttpRe
.unwrap_or(&Text(String::from("")))
.0
.clone();
log::info!("{first} {last} signed up for the local mission trip!");
info!("{first} {last} signed up for the local mission trip!");
let email = markup::new! {
@markup::doctype()
html {
@ -260,7 +259,7 @@ pub async fn local_form(MultipartForm(form): MultipartForm<LocalForm>) -> HttpRe
}
}
};
log::info!("{:?}", form);
info!("{:?}", form);
let multi = MultiPart::alternative_plain_html(String::from("Testing"), email.to_string());
if let Ok(m) = Message::builder()
@ -274,21 +273,9 @@ pub async fn local_form(MultipartForm(form): MultipartForm<LocalForm>) -> HttpRe
.subject(email_subject)
.multipart(multi)
{
let sender = SmtpTransport::relay("mail.tfcconnection.org")
.ok()
.unwrap()
.credentials(Credentials::new(
"no-reply@mail.tfcconnection.org".to_owned(),
"r9f36mNZFtiW4f".to_owned(),
))
.authentication(vec![Mechanism::Plain])
.build();
match sender.send(&m) {
Ok(res) => log::info!("{:?}", res),
Err(e) => log::info!("{e}"),
}
let _ = send_email(m);
} else {
log::info!("Email incorrect");
info!("Email incorrect");
}
HttpResponse::Ok().body("thankyou")

View file

@ -1,8 +1,9 @@
pub mod camp_form;
pub mod church_form;
pub mod errors;
// pub mod errors;
pub mod contact;
pub mod health_form;
pub mod local_trip_form;
pub mod mt_church_form;
pub mod mt_form;
pub mod parent_form;
pub mod teacher_form;
pub mod mt_parent_form;
pub mod mt_teacher_form;

234
src/api/mt_church_form.rs Normal file
View file

@ -0,0 +1,234 @@
use std::{
collections::{BTreeMap, HashMap},
fs,
};
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
use actix_web::{post, web, HttpResponse};
use color_eyre::{eyre::eyre, Result};
use lettre::{
message::{header::ContentType, Attachment, MultiPart, SinglePart},
Message,
};
use maud::{html, Markup, PreEscaped, DOCTYPE};
use reqwest::Client;
use serde_json::json;
use sqlx::SqliteConnection;
use tracing::{error, info};
#[derive(Debug, MultipartForm)]
struct MtChurchForm {
#[multipart(rename = "firstname")]
first_name: Text<String>,
#[multipart(rename = "lastname")]
last_name: Text<String>,
#[multipart(rename = "studentfirstname")]
student_first_name: Text<String>,
#[multipart(rename = "studentlastname")]
student_last_name: Text<String>,
relationship: Text<String>,
#[multipart(rename = "walk-with-jesus")]
walk_jesus: Text<String>,
commitment: Text<String>,
#[multipart(rename = "pos-characteristics")]
positive: Text<String>,
#[multipart(rename = "neg-characteristics")]
negative: Text<String>,
teachable: Text<String>,
#[multipart(rename = "extra-info")]
extra_info: Text<String>,
}
impl From<&MtChurchForm> for HashMap<i32, String> {
fn from(form: &MtChurchForm) -> Self {
let mut map = HashMap::new();
map.insert(158, format!("{} {}", form.first_name.0, form.last_name.0));
map.insert(
159,
format!("{} {}", form.student_first_name.0, form.student_last_name.0),
);
map.insert(160, form.relationship.0.clone());
map.insert(163, form.positive.0.clone());
map.insert(164, form.negative.0.clone());
map.insert(161, form.walk_jesus.0.clone());
map.insert(162, form.commitment.0.clone());
map.insert(165, form.teachable.0.clone());
map.insert(166, form.extra_info.0.clone());
map
}
}
impl MtChurchForm {
async fn build_email(&self) -> Markup {
html! {
(DOCTYPE)
meta charset="utf-8";
html {
head {
title { (self.first_name.0) " " (self.last_name.0) " filled out a church reference form for " (self.student_first_name.0) " " (self.student_last_name.0) "!" }
style {
"table { border-collapse: collapse; width: 100% }"
"td, th { padding: 8px }"
"td { text-align: left; width: 70%; word-wrap: break-word }"
"th { text-align: right; border-right: 1px solid #ddd }"
"tr { border-bottom: 1px solid #ddd }"
"h1 { text-align: center }"
}
}
body {
h1 { "Church reference form for " (self.student_first_name.0) " " (self.student_last_name.0) "!" }
hr;
table {
tr {
th { "Name" }
td { (self.first_name.0) " " (self.last_name.0) }
}
tr {
th { "Student" }
td { (self.student_first_name.0) " " (self.student_last_name.0) }
}
tr {
th { "Relationship with student" }
td { (self.relationship.0) }
}
tr {
th { "Positive characteristics" }
td { (self.positive.0) }
}
tr {
th { "Negative characteristics" }
td { (self.negative.0) }
}
tr {
th { "Walk with Jesus" }
td { (self.walk_jesus.0) }
}
tr {
th { "Commitment" }
td { (self.commitment.0) }
}
tr {
th { "Teachable heart" }
td { (self.teachable.0) }
}
tr {
th { "Other Relevant Info" }
td { (self.extra_info.0) }
}
}
}
}
}
}
async fn store_form(&self) -> Result<()> {
let client = Client::new();
let map = HashMap::from(self);
let mut json = HashMap::new();
json.insert("data", map);
let link = r#"https://staff.tfcconnection.org/apps/tables/#/table/13/row/757"#;
let res = client
.post("https://staff.tfcconnection.org/ocs/v2.php/apps/tables/api/2/tables/13/rows")
.basic_auth("chris", Some("2VHeGxeC^Zf9KqFK^G@Pt!zu2q^6@b"))
.header("OCS-APIRequest", "true")
.header("Content-Type", "application/json")
.json(&json)
.send()
.await?;
if res.status().is_success() {
let res = res.text().await.unwrap();
Ok(())
} else {
Err(eyre!(
"Problem in storing data: {:?}",
res.error_for_status()
))
}
}
async fn send_email(&mut self) -> Result<()> {
let first = self.student_first_name.clone();
let last = self.student_last_name.clone();
let email_subject = format!("Church reference form for {} {}!", first, last);
info!("{first} {last} church reference form!");
let email = self.build_email().await;
let email = SinglePart::html(email.into_string());
if let Ok(m) = Message::builder()
.from(
"TFC ADMIN <no-reply@mail.tfcconnection.org>"
.parse()
.unwrap(),
)
.to("Chris Cochrun <chris@tfcconnection.org>".parse().unwrap())
.to("Ethan Rose <ethan@tfcconnection.org>".parse().unwrap())
.subject(email_subject)
.singlepart(email)
{
crate::email::send_email(m).await
} else {
Err(eyre!("Email incorrect"))
}
}
}
#[post("/api/mt-church-form")]
pub async fn mt_church_form(MultipartForm(mut form): MultipartForm<MtChurchForm>) -> HttpResponse {
match form.store_form().await {
Ok(_) => info!("Successfully sent form to nextcloud!"),
Err(e) => error!("There was an erroring sending form to nextcloud: {e}"),
}
match form.send_email().await {
Ok(_) => info!("Successfully sent email"),
Err(e) => error!("There was an error sending the email: {e}"),
}
HttpResponse::Ok().body("thankyou")
}
#[cfg(test)]
mod test {
use actix_web::test;
use pretty_assertions::assert_eq;
use sqlx::Connection;
use tracing::debug;
use super::*;
fn form() -> MtChurchForm {
MtChurchForm {
first_name: Text(String::from("Bilbo")),
last_name: Text(String::from("Braggins")),
student_first_name: Text(String::from("Frodo")),
student_last_name: Text(String::from("Braggins")),
relationship: Text(String::from("Uncle")),
positive: Text(String::from("Nimble and brave")),
negative: Text(String::from("Small")),
walk_jesus: Text(String::from("Such a strutter")),
commitment: Text(String::from("Super")),
teachable: Text(String::from("Very")),
extra_info: Text(String::from("Willing to take the ring")),
}
}
#[test]
async fn test_nc_post() {
let form = form();
assert!(!form.first_name.is_empty());
let res = form.store_form().await;
match res {
Ok(_) => assert!(true, "passed storing test"),
Err(e) => assert!(false, "Failed storing test: {e}"),
}
}
#[test]
async fn test_email() {
let mut form = form();
assert!(!form.first_name.is_empty());
match form.send_email().await {
Ok(_) => assert!(true, "passed emailing test"),
Err(e) => assert!(false, "Failed emailing test: {e}"),
}
}
}

View file

@ -1,487 +1,519 @@
use std::fs;
use std::{collections::HashMap, fs};
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
use actix_web::{post, HttpResponse};
use color_eyre::{
eyre::{eyre, Context},
Result,
};
use futures::FutureExt;
use lettre::{
message::{header::ContentType, Attachment, MultiPart, SinglePart},
transport::smtp::authentication::{Credentials, Mechanism},
Message, SmtpTransport, Transport,
Message,
};
use maud::{html, Markup, DOCTYPE};
use reqwest::Client;
use tracing::{error, info};
#[derive(Debug, MultipartForm, Default)]
#[derive(Debug, MultipartForm)]
struct MtForm {
#[multipart(rename = "firstname")]
first_name: Option<Text<String>>,
first_name: Text<String>,
#[multipart(rename = "lastname")]
last_name: Option<Text<String>>,
last_name: Text<String>,
#[multipart(rename = "parentfirstname")]
parent_first_name: Option<Text<String>>,
parent_first_name: Text<String>,
#[multipart(rename = "parentlastname")]
parent_last_name: Option<Text<String>>,
birthdate: Option<Text<String>>,
gender: Option<Text<String>>,
street: Option<Text<String>>,
city: Option<Text<String>>,
state: Option<Text<String>>,
zip: Option<Text<i32>>,
cellphone: Option<Text<String>>,
parentphone: Option<Text<String>>,
email: Option<Text<String>>,
parentemail: Option<Text<String>>,
school: Option<Text<String>>,
grade: Option<Text<String>>,
parent_last_name: Text<String>,
birthdate: Text<String>,
gender: Text<String>,
street: Text<String>,
city: Text<String>,
state: Text<String>,
zip: Text<i32>,
cellphone: Text<String>,
parentphone: Text<String>,
email: Text<String>,
parentemail: Text<String>,
school: Text<String>,
grade: Text<String>,
#[multipart(rename = "pastorfirstname")]
pastor_first_name: Option<Text<String>>,
pastor_first_name: Text<String>,
#[multipart(rename = "pastorlastname")]
pastor_last_name: Option<Text<String>>,
pastor_last_name: Text<String>,
#[multipart(rename = "churchattendance")]
church_attendance: Option<Text<String>>,
church_attendance: Text<String>,
#[multipart(rename = "tfcgroup")]
tfc_group: Option<Text<String>>,
shirt: Option<Text<String>>,
trip: Option<Text<String>>,
tfc_group: Text<String>,
shirt: Text<String>,
trip: Text<String>,
#[multipart(rename = "tripnotes")]
trip_notes: Option<Text<String>>,
trip_notes: Text<String>,
#[multipart(rename = "relationship-with-jesus")]
relationship_with_jesus: Option<Text<String>>,
relationship_with_jesus: Text<String>,
#[multipart(rename = "testimony")]
testimony: Option<Text<String>>,
testimony: Text<String>,
#[multipart(rename = "involvement-with-group")]
involvement_with_group: Option<Text<String>>,
involvement_with_group: Text<String>,
#[multipart(rename = "reasons-for-trip-choice")]
reasons: Option<Text<String>>,
strengths: Option<Text<String>>,
weaknesses: Option<Text<String>>,
reasons: Text<String>,
strengths: Text<String>,
weaknesses: Text<String>,
#[multipart(rename = "previous-trip-info")]
previous_trip_info: Option<Text<String>>,
previous_trip_info: Text<String>,
#[multipart(rename = "attitude-torward-work")]
attitude: Option<Text<String>>,
attitude: Text<String>,
#[multipart(rename = "relevant-notes")]
relevant_notes: Option<Text<String>>,
relevant_notes: Text<String>,
#[multipart(rename = "final-agreement")]
final_agreement: Option<Text<String>>,
registration: Option<Text<String>>,
final_agreement: Text<String>,
registration: Text<String>,
#[multipart(rename = "health-form")]
health_form: Text<String>,
#[multipart(rename = "image")]
file: Option<TempFile>,
}
#[post("/mt-form")]
pub async fn mt_form(MultipartForm(form): MultipartForm<MtForm>) -> HttpResponse {
let first = form
.first_name
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let last = form
.last_name
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let email_subject = format!("{} {} signed up for mission trip!", first, last);
let filename_noext = format!("{}_{}", first, last);
let parent = format!(
"{} {}",
form.parent_first_name
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone(),
form.parent_last_name
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone()
);
let birthdate = form
.birthdate
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let gender = form
.gender
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let street = form
.street
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let city = form
.city
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let state = form
.state
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let zip = form.zip.as_ref().unwrap_or(&Text(0)).0;
let cellphone = form
.cellphone
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let parentphone = form
.parentphone
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let email = form
.email
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let parentemail = form
.parentemail
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let school = form
.school
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let grade = form
.grade
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let pastor = format!(
"{} {}",
form.pastor_first_name
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone(),
form.pastor_last_name
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone()
);
let church_attendance = form
.church_attendance
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let tfc_group = form
.tfc_group
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let shirt = form
.shirt
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let trip = form
.trip
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let trip_notes = form
.trip_notes
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let relationship = form
.relationship_with_jesus
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let testimony = form
.testimony
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let involvement = form
.involvement_with_group
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let reasons = form
.reasons
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let strengths = form
.strengths
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let weaknesses = form
.weaknesses
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let previous_trip = form
.previous_trip_info
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let attitude = form
.attitude
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let relevant = form
.relevant_notes
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let final_agreement = form
.final_agreement
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
let registration = form
.registration
.as_ref()
.unwrap_or(&Text(String::from("")))
.0
.clone();
log::info!("{first} {last} signed up for mission trip!");
let email = markup::new! {
@markup::doctype()
html {
head {
title { @format!("{} {} signed up for mission trip!", first, last) }
style {
"table { border-collapse: collapse; width: 100% }"
"td, th { padding: 8px }"
"td { text-align: left; width: 70%; word-wrap: break-word }"
"th { text-align: right; border-right: 1px solid #ddd }"
"tr { border-bottom: 1px solid #ddd }"
"h1 { text-align: center }"
}
}
body {
h1 { @format!("Mission trip form for {} {}!", first, last) }
hr;
table {
tr {
th { "Name" }
td { @format!("{} {}", first, last) }
}
tr {
th { "Parent" }
td { @parent }
}
tr {
th { "Birthdate" }
td { @birthdate }
}
tr {
th { "Gender" }
td { @gender }
}
tr {
th { "Street" }
td { @street }
}
tr {
th { "City" }
td { @city }
}
tr {
th { "State" }
td { @state }
}
tr {
th { "Zip" }
td { @zip }
}
tr {
th { "Phone" }
td { @cellphone }
}
tr {
th { "Parent Phone" }
td { @parentphone }
}
tr {
th { "Email" }
td { @email }
}
tr {
th { "Parent Email" }
td { @parentemail }
}
tr {
th { "School" }
td { @school }
}
tr {
th { "Grade" }
td { @grade }
}
tr {
th { "Pastor" }
td { @pastor }
}
tr {
th { "Church Attendance" }
td { @church_attendance }
}
tr {
th { "TFC Group" }
td { @tfc_group }
}
tr {
th { "T-Shirt Size" }
td { @shirt }
}
tr {
th { "Trip Choice" }
td { @trip }
}
tr {
th { "Extra Trip Notes" }
td { @trip_notes }
}
tr {
th { "Relationship with Jesus" }
td { @relationship }
}
tr {
th { "Testimony" }
td { @testimony }
}
tr {
th { "Involvement with TFC or Youth Group" }
td { @involvement }
}
tr {
th { "Reasons for trip choice" }
td { @reasons }
}
tr {
th { "Strengths" }
td { @strengths }
}
tr {
th { "Weaknesses" }
td { @weaknesses }
}
tr {
th { "Previous Trips" }
td { @previous_trip }
}
tr {
th { "Attitude Torward Work" }
td { @attitude }
}
tr {
th { "Other Relevant Info" }
td { @relevant }
}
tr {
th { "Final Agreement" }
td { @final_agreement }
}
tr {
th { "Registration" }
td { @registration }
}
}
}
}
};
let mut path: Option<String> = Some(String::from(""));
let mut file_exists = false;
let mut filename = String::from("");
log::info!("{:?}", file_exists);
if let Some(f) = form.file {
if let Some(file) = f.file_name {
if let Some(ext) = file.rsplit('.').next() {
filename = format!("{}.{}", filename_noext, ext);
path = Some(format!("./tmp/{}.{}", filename_noext, ext));
} else {
path = Some(format!("./tmp/{}", file));
}
// let path = format!("./tmp/{}", file);
log::info!("saving to {}", path.clone().unwrap());
match f.file.persist(path.clone().unwrap()) {
Ok(f) => {
log::info!("{:?}", f);
if f.metadata().unwrap().len() > 0 {
file_exists = true;
}
}
Err(e) => log::info!("{:?}: Probably a missing image", e),
}
}
impl From<&MtForm> for HashMap<i32, String> {
fn from(form: &MtForm) -> Self {
let mut map = HashMap::new();
map.insert(106, format!("{} {}", form.first_name.0, form.last_name.0));
map.insert(
107,
format!("{} {}", form.parent_first_name.0, form.parent_last_name.0),
);
map.insert(109, form.gender.0.clone());
map.insert(110, form.birthdate.0.clone());
map.insert(117, form.street.0.clone());
map.insert(118, form.city.0.clone());
map.insert(119, form.zip.0.to_string());
map.insert(120, form.state.0.clone());
map.insert(121, form.cellphone.0.clone());
map.insert(122, form.email.0.clone());
map.insert(123, form.parentphone.0.clone());
map.insert(124, form.parentemail.0.clone());
map.insert(125, form.school.0.clone());
map.insert(126, form.grade.0.clone());
map.insert(
127,
format!("{} {}", form.pastor_first_name.0, form.pastor_last_name.0),
);
map.insert(128, form.church_attendance.0.clone());
map.insert(129, form.tfc_group.0.clone());
map.insert(130, form.shirt.0.clone());
map.insert(131, form.trip.0.clone());
map.insert(132, form.trip_notes.0.clone());
map.insert(133, form.relationship_with_jesus.0.clone());
map.insert(134, form.testimony.0.clone());
map.insert(135, form.involvement_with_group.0.clone());
map.insert(136, form.reasons.0.clone());
map.insert(137, form.strengths.0.clone());
map.insert(138, form.weaknesses.0.clone());
map.insert(139, form.previous_trip_info.0.clone());
map.insert(140, form.attitude.0.clone());
map.insert(141, form.relevant_notes.0.clone());
map.insert(144, form.final_agreement.0.clone());
map.insert(145, form.registration.0.clone());
map
}
}
impl MtForm {
fn build_email(&self) -> Markup {
html! {
(DOCTYPE)
meta charset="utf-8";
html {
head {
title { (self.first_name.0) " " (self.last_name.0) " signed up for mission trip!" }
style {
"table { border-collapse: collapse; width: 100% }"
"td, th { padding: 8px }"
"td { text-align: left; width: 70%; word-wrap: break-word }"
"th { text-align: right; border-right: 1px solid #ddd }"
"tr { border-bottom: 1px solid #ddd }"
"h1 { text-align: center }"
}
}
body {
h1 { "Mission trip form for " (self.first_name.0) " " (self.last_name.0) "!" }
hr;
table {
tr {
th { "Name" }
td { (self.first_name.0) " " (self.last_name.0) }
}
tr {
th { "Parent" }
td { (self.parent_first_name.0) " " (self.parent_last_name.0) }
}
tr {
th { "Birthdate" }
td { (self.birthdate.0) }
}
tr {
th { "Gender" }
td { (self.gender.0) }
}
tr {
th { "Street" }
td { (self.street.0) }
}
tr {
th { "City" }
td { (self.city.0) }
}
tr {
th { "State" }
td { (self.state.0) }
}
tr {
th { "Zip" }
td { (self.zip.0) }
}
tr {
th { "Phone" }
td { (self.cellphone.0) }
}
tr {
th { "Parent Phone" }
td { (self.parentphone.0) }
}
tr {
th { "Email" }
td { (self.email.0) }
}
tr {
th { "Parent Email" }
td { (self.parentemail.0) }
}
tr {
th { "School" }
td { (self.school.0) }
}
tr {
th { "Grade" }
td { (self.grade.0) }
}
tr {
th { "Pastor" }
td { (self.pastor_first_name.0) (self.pastor_last_name.0) }
}
tr {
th { "Church Attendance" }
td { (self.church_attendance.0) }
}
tr {
th { "TFC Group" }
td { (self.tfc_group.0) }
}
tr {
th { "T-Shirt Size" }
td { (self.shirt.0) }
}
tr {
th { "Trip Choice" }
td { (self.trip.0) }
}
tr {
th { "Extra Trip Notes" }
td { (self.trip_notes.0) }
}
tr {
th { "Relationship with Jesus" }
td { (self.relationship_with_jesus.0) }
}
tr {
th { "Testimony" }
td { (self.testimony.0) }
}
tr {
th { "Involvement with TFC or Youth Group" }
td { (self.involvement_with_group.0) }
}
tr {
th { "Reasons for trip choice" }
td { (self.reasons.0) }
}
tr {
th { "Strengths" }
td { (self.strengths.0) }
}
tr {
th { "Weaknesses" }
td { (self.weaknesses.0) }
}
tr {
th { "Previous Trips" }
td { (self.previous_trip_info.0) }
}
tr {
th { "Attitude Torward Work" }
td { (self.attitude.0) }
}
tr {
th { "Other Relevant Info" }
td { (self.relevant_notes.0) }
}
tr {
th { "Final Agreement" }
td { (self.final_agreement.0) }
}
tr {
th { "Registration" }
td { (self.registration.0) }
}
}
}
}
}
}
fn get_temp_file(&self) -> Option<(String, String, Option<String>)> {
let first = self.first_name.clone();
let last = self.last_name.clone();
let filename_noext = format!("{}_{}", first, last);
let (file_name, content_type) = if let Some(file) = self.file.as_ref() {
let content_type = file.content_type.clone().map(|m| m.to_string());
(file.file_name.to_owned(), content_type)
} else {
return None;
};
let filename;
let path = if let Some(file_name) = file_name {
if let Some(ext) = file_name.rsplit('.').next() {
filename = format!("{}.{}", filename_noext, ext);
format!("./tmp/{}.{}", filename_noext, ext)
} else {
filename = String::default();
format!("./tmp/{}", file_name)
}
} else {
filename = String::default();
String::default()
};
if let Some(file) = &self.file {
let file = file.file.path();
match fs::copy(file, &path) {
Ok(f) => {
if f <= 0 {
return None;
}
info!(?f, "File saved successfully");
Some((filename, path, content_type.clone()))
}
Err(e) => {
error!("{:?}: Probably a missing image", e);
None
}
}
} else {
error!("Error in tempfile");
None
}
}
fn prepare_email(&self) -> Result<Message> {
let first = self.first_name.clone();
let last = self.last_name.clone();
let email_subject = format!("{} {} signed up for mission trip!", first, last);
info!("{first} {last} signed up for mission trip!");
let email = self.build_email();
let temp_file = self.get_temp_file();
let multi = if let Some((file, path, content_type)) = temp_file {
let filebody = fs::read(path);
let content_type =
ContentType::parse(&content_type.unwrap_or(String::from("image/jpg"))).unwrap();
let attachment = Attachment::new(file).body(filebody.unwrap(), content_type);
// info!(?attachment);
MultiPart::mixed()
.singlepart(SinglePart::html(email.into_string()))
.singlepart(attachment)
} else {
MultiPart::alternative_plain_html(String::from("Testing"), email.into_string())
};
Message::builder()
.from(
"TFC ADMIN <no-reply@mail.tfcconnection.org>"
.parse()
.unwrap(),
)
.to("Chris Cochrun <chris@tfcconnection.org>".parse().unwrap())
.to("Ethan Rose <ethan@tfcconnection.org>".parse().unwrap())
.subject(email_subject)
.multipart(multi)
.wrap_err("problemss")
}
}
#[post("/api/mt-form")]
pub async fn mt_form(MultipartForm(form): MultipartForm<MtForm>) -> HttpResponse {
let name = format!("{} {}", form.first_name.0, form.last_name.0);
let map = HashMap::from(&form);
let store = store_form(map);
actix_rt::spawn(store.map(|s| match s {
Ok(_) => info!("Successfully sent form to nextcloud!"),
Err(e) => error!("There was an erroring sending form to nextcloud: {e}"),
}));
let email = form.prepare_email();
match email {
Ok(m) => {
let sent = crate::email::send_email(m);
actix_rt::spawn(sent.map(|s| match s {
Ok(_) => info!("Successfully sent form to email!"),
Err(e) => error!("There was an erroring sending form to email: {e}"),
}));
}
Err(e) => error!("error sending email {e}"),
};
match form.registration.0.as_str() {
"now" => {
if form.health_form.0.as_str() == "yes" {
HttpResponse::Ok()
.insert_header(("Access-Control-Expose-Headers", "*"))
.insert_header(("HX-Redirect", "/health-form?registration=now"))
.finish()
} else {
HttpResponse::Ok()
.insert_header(("Access-Control-Expose-Headers", "*"))
.insert_header((
"HX-Redirect",
"https://secure.myvanco.com/L-Z772/campaign/C-13DM3",
))
.finish()
}
}
"later" => {
if form.health_form.0.as_str() == "yes" {
HttpResponse::Ok()
.insert_header(("Access-Control-Expose-Headers", "*"))
.insert_header(("HX-Redirect", "/health-form?registration=later"))
.finish()
} else {
HttpResponse::Ok().body(
html! {
h2 { "Thank you! {}" (name)}
p { "You can go to the health form "
a href="/health-form" { "here" }
" or you can pay for mission trip "
a href="https://secure.myvanco.com/L-Z772/campaign/C-13DM3" { "here" }
}
}
.into_string(),
)
}
}
_ => {
error!("There wasn't an option for the registration passed in.");
HttpResponse::Ok().body(
html! {
h2 { "Thank you! {}" (name)}
p { "You can go to the health form "
a href="/health-form" { "here" }
" or you can pay for mission trip "
a href="https://secure.myvanco.com/L-Z772/campaign/C-13DM3" { "here" }
}
}
.into_string(),
)
}
}
}
async fn store_form(map: HashMap<i32, String>) -> Result<()> {
let client = Client::new();
// let map = HashMap::from(self);
let mut json = HashMap::new();
json.insert("data", map);
let res = client
.post("https://staff.tfcconnection.org/ocs/v2.php/apps/tables/api/2/tables/9/rows")
.basic_auth("chris", Some("2VHeGxeC^Zf9KqFK^G@Pt!zu2q^6@b"))
.header("OCS-APIRequest", "true")
.header("Content-Type", "application/json")
.json(&json)
.send()
.await?;
if res.status().is_success() {
// let res = res.text().await.unwrap();
Ok(())
} else {
Err(eyre!(
"Problem in storing data: {:?}",
res.error_for_status()
))
}
}
#[cfg(test)]
mod test {
use actix_web::test;
use super::*;
fn form() -> MtForm {
MtForm {
first_name: Text(String::from("Frodo")),
last_name: Text(String::from("Braggins")),
parent_first_name: Text(String::from("Bilbo")),
parent_last_name: Text(String::from("Braggins")),
birthdate: Text(String::from("1845-09-12")),
gender: Text(String::from("male")),
street: Text(String::from("1234 Bag End")),
city: Text(String::from("The Shire")),
state: Text(String::from("Hobbiton")),
zip: Text(88888),
cellphone: Text(String::from("7868889797")),
parentphone: Text(String::from("1234567898")),
email: Text(String::from("frodo@hobbits.com")),
parentemail: Text(String::from("bilbo@hobbits.com")),
school: Text(String::from("Shire High")),
grade: Text(String::from("junior")),
pastor_first_name: Text(String::from("Gandalf")),
pastor_last_name: Text(String::from("The White")),
church_attendance: Text(String::from("often")),
tfc_group: Text(String::from("Northern Valley")),
shirt: Text(String::from("medium")),
trip: Text(String::from("Mordor")),
trip_notes: Text(String::from("If it must happen, I'll do it.")),
relationship_with_jesus: Text(String::from("Cool beans")),
testimony: Text(String::from("Nephew of Bilbo Braggins")),
involvement_with_group: Text(String::from("Good friends with Gandalf")),
reasons: Text(String::from("Want an adventure")),
strengths: Text(String::from("Willing, brave, small, and curious")),
weaknesses: Text(String::from("Not strong, or good with weapons")),
previous_trip_info: Text(String::from("The edge of Hob Hill")),
attitude: Text(String::from("Willing")),
relevant_notes: Text(String::from("Willing to take the ring")),
final_agreement: Text(String::from("yes")),
registration: Text(String::from("later")),
health_form: Text(String::from("yes")),
file: None,
}
}
#[test]
async fn test_nc_post() {
let form = form();
assert!(!form.first_name.is_empty());
let map = HashMap::from(&form);
let res = store_form(map).await;
match res {
Ok(_) => assert!(true, "passed storing test"),
Err(e) => assert!(false, "Failed storing test: {e}"),
}
}
#[test]
async fn test_email() {
let form = form();
assert!(!form.first_name.is_empty());
match form.prepare_email() {
Ok(m) => {
assert!(crate::email::send_email(m).await.is_ok())
}
Err(e) => assert!(false, "Failed emailing test: {e}"),
}
}
let multi = if file_exists {
let filebody = fs::read(path.clone().unwrap_or_default());
let content_type = ContentType::parse("image/jpg").unwrap();
let attachment = Attachment::new(filename).body(filebody.unwrap(), content_type);
log::info!("{:?}", attachment);
MultiPart::mixed()
.singlepart(SinglePart::html(email.to_string()))
.singlepart(attachment)
} else {
MultiPart::alternative_plain_html(String::from("Testing"), email.to_string())
};
if let Ok(m) = Message::builder()
.from(
"TFC ADMIN <no-reply@mail.tfcconnection.org>"
.parse()
.unwrap(),
)
.to("Chris Cochrun <chris@tfcconnection.org>".parse().unwrap())
.to("Ethan Rose <ethan@tfcconnection.org>".parse().unwrap())
.subject(email_subject)
.multipart(multi)
{
let sender = SmtpTransport::relay("mail.tfcconnection.org")
.ok()
.unwrap()
.credentials(Credentials::new(
"no-reply@mail.tfcconnection.org".to_owned(),
"r9f36mNZFtiW4f".to_owned(),
))
.authentication(vec![Mechanism::Plain])
.build();
match sender.send(&m) {
Ok(res) => log::info!("{:?}", res),
Err(e) => log::info!("{e}"),
}
} else {
log::info!("Email incorrect");
}
HttpResponse::Ok().body("thankyou")
}

241
src/api/mt_parent_form.rs Normal file
View file

@ -0,0 +1,241 @@
use std::{
collections::{BTreeMap, HashMap},
fs,
};
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
use actix_web::{post, web, HttpResponse};
use color_eyre::{eyre::eyre, Result};
use lettre::{
message::{header::ContentType, Attachment, MultiPart, SinglePart},
Message,
};
use maud::{html, Markup, PreEscaped, DOCTYPE};
use reqwest::Client;
use serde_json::json;
use sqlx::SqliteConnection;
use tracing::{error, info};
#[derive(Debug, MultipartForm)]
struct MtParentForm {
#[multipart(rename = "firstname")]
first_name: Text<String>,
#[multipart(rename = "lastname")]
last_name: Text<String>,
email: Text<String>,
#[multipart(rename = "studentfirstname")]
student_first_name: Text<String>,
#[multipart(rename = "studentlastname")]
student_last_name: Text<String>,
authority: Text<String>,
positive: Text<String>,
negative: Text<String>,
#[multipart(rename = "family-relation")]
family: Text<String>,
#[multipart(rename = "previous-trip-info")]
previous_trip: Text<String>,
#[multipart(rename = "trip-feelings")]
feelings: Text<String>,
#[multipart(rename = "extra-info")]
extra_info: Text<String>,
}
impl From<&MtParentForm> for HashMap<i32, String> {
fn from(form: &MtParentForm) -> Self {
let mut map = HashMap::new();
map.insert(167, format!("{} {}", form.first_name.0, form.last_name.0));
map.insert(
168,
format!("{} {}", form.student_first_name.0, form.student_last_name.0),
);
map.insert(169, form.authority.0.clone());
map.insert(170, form.positive.0.clone());
map.insert(171, form.negative.0.clone());
map.insert(172, form.family.0.clone());
map.insert(173, form.previous_trip.0.clone());
map.insert(174, form.feelings.0.clone());
map.insert(175, form.extra_info.0.clone());
map.insert(176, form.email.0.clone());
map
}
}
impl MtParentForm {
async fn build_email(&self) -> Markup {
html! {
(DOCTYPE)
meta charset="utf-8";
html {
head {
title { (self.first_name.0) " " (self.last_name.0) " filled out a parent form for " (self.student_first_name.0) " " (self.student_last_name.0) "!" }
style {
"table { border-collapse: collapse; width: 100% }"
"td, th { padding: 8px }"
"td { text-align: left; width: 70%; word-wrap: break-word }"
"th { text-align: right; border-right: 1px solid #ddd }"
"tr { border-bottom: 1px solid #ddd }"
"h1 { text-align: center }"
}
}
body {
h1 { "Parent reference form for " (self.student_first_name.0) " " (self.student_last_name.0) "!" }
hr;
table {
tr {
th { "Name" }
td { (self.first_name.0) " " (self.last_name.0) }
}
tr {
th { "Student" }
td { (self.student_first_name.0) " " (self.student_last_name.0) }
}
tr {
th { "Email" }
td { (self.email.0) }
}
tr {
th { "Authority" }
td { (self.authority.0) }
}
tr {
th { "Positive characteristics" }
td { (self.positive.0) }
}
tr {
th { "Negative characteristics" }
td { (self.negative.0) }
}
tr {
th { "Family Relations" }
td { (self.family.0) }
}
tr {
th { "Previous Trip" }
td { (self.previous_trip.0) }
}
tr {
th { "Trip Feelings" }
td { (self.feelings.0) }
}
tr {
th { "Other Relevant Info" }
td { (self.extra_info.0) }
}
}
}
}
}
}
async fn store_form(&self) -> Result<()> {
let client = Client::new();
let map = HashMap::from(self);
let mut json = HashMap::new();
json.insert("data", map);
let link = r#"https://staff.tfcconnection.org/apps/tables/#/table/14/row/757"#;
let res = client
.post("https://staff.tfcconnection.org/ocs/v2.php/apps/tables/api/2/tables/14/rows")
.basic_auth("chris", Some("2VHeGxeC^Zf9KqFK^G@Pt!zu2q^6@b"))
.header("OCS-APIRequest", "true")
.header("Content-Type", "application/json")
.json(&json)
.send()
.await?;
if res.status().is_success() {
let res = res.text().await.unwrap();
Ok(())
} else {
Err(eyre!(
"Problem in storing data: {:?}",
res.error_for_status()
))
}
}
async fn send_email(&self) -> Result<()> {
let first = self.student_first_name.clone();
let last = self.student_last_name.clone();
let email_subject = format!("Parent reference form for {} {}!", first, last);
info!("{first} {last} parent reference form!");
let email = self.build_email().await;
let email = SinglePart::html(email.into_string());
if let Ok(m) = Message::builder()
.from(
"TFC ADMIN <no-reply@mail.tfcconnection.org>"
.parse()
.unwrap(),
)
.to("Chris Cochrun <chris@tfcconnection.org>".parse().unwrap())
.to("Ethan Rose <ethan@tfcconnection.org>".parse().unwrap())
.subject(email_subject)
.singlepart(email)
{
crate::email::send_email(m).await
} else {
Err(eyre!("Email incorrect"))
}
}
}
#[post("/api/mt-parent-form")]
pub async fn mt_parent_form(MultipartForm(form): MultipartForm<MtParentForm>) -> HttpResponse {
match form.store_form().await {
Ok(_) => info!("Successfully sent form to nextcloud!"),
Err(e) => error!("There was an erroring sending form to nextcloud: {e}"),
}
match form.send_email().await {
Ok(_) => info!("Successfully sent email"),
Err(e) => error!("There was an error sending the email: {e}"),
}
HttpResponse::Ok().body("thankyou")
}
#[cfg(test)]
mod test {
use actix_web::test;
use pretty_assertions::assert_eq;
use sqlx::Connection;
use tracing::debug;
use super::*;
fn form() -> MtParentForm {
MtParentForm {
first_name: Text(String::from("Bilbo")),
last_name: Text(String::from("Braggins")),
student_first_name: Text(String::from("Frodo")),
student_last_name: Text(String::from("Braggins")),
email: Text(String::from("biblo@hobbits.us")),
authority: Text(String::from("Uncle")),
positive: Text(String::from("Nimble and brave")),
negative: Text(String::from("Small")),
family: Text(String::from("Such a strutter")),
previous_trip: Text(String::from("Super")),
feelings: Text(String::from("Very")),
extra_info: Text(String::from("Willing to take the ring")),
}
}
#[test]
async fn test_nc_post() {
let form = form();
assert!(!form.first_name.is_empty());
let res = form.store_form().await;
match res {
Ok(_) => assert!(true, "passed storing test"),
Err(e) => assert!(false, "Failed storing test: {e}"),
}
}
#[test]
async fn test_email() {
let mut form = form();
assert!(!form.first_name.is_empty());
match form.send_email().await {
Ok(_) => assert!(true, "passed emailing test"),
Err(e) => assert!(false, "Failed emailing test: {e}"),
}
}
}

234
src/api/mt_teacher_form.rs Normal file
View file

@ -0,0 +1,234 @@
use std::{
collections::{BTreeMap, HashMap},
fs,
};
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
use actix_web::{post, web, HttpResponse};
use color_eyre::{eyre::eyre, Result};
use lettre::{
message::{header::ContentType, Attachment, MultiPart, SinglePart},
Message,
};
use maud::{html, Markup, PreEscaped, DOCTYPE};
use reqwest::Client;
use serde_json::json;
use sqlx::SqliteConnection;
use tracing::{error, info};
#[derive(Debug, MultipartForm)]
struct MtTeacherForm {
#[multipart(rename = "firstname")]
first_name: Text<String>,
#[multipart(rename = "lastname")]
last_name: Text<String>,
#[multipart(rename = "studentfirstname")]
student_first_name: Text<String>,
#[multipart(rename = "studentlastname")]
student_last_name: Text<String>,
relationship: Text<String>,
positive: Text<String>,
negative: Text<String>,
attitudes: Text<String>,
#[multipart(rename = "team-challenges")]
challenges: Text<String>,
behavior: Text<String>,
#[multipart(rename = "extra-info")]
extra_info: Text<String>,
}
impl From<&MtTeacherForm> for HashMap<i32, String> {
fn from(form: &MtTeacherForm) -> Self {
let mut map = HashMap::new();
map.insert(150, format!("{} {}", form.first_name.0, form.last_name.0));
map.insert(
151,
format!("{} {}", form.student_first_name.0, form.student_last_name.0),
);
map.insert(152, form.relationship.0.clone());
map.insert(153, form.positive.0.clone());
map.insert(154, form.negative.0.clone());
map.insert(155, form.attitudes.0.clone());
map.insert(156, form.challenges.0.clone());
map.insert(157, form.extra_info.0.clone());
map.insert(177, form.behavior.0.clone());
map
}
}
impl MtTeacherForm {
async fn build_email(&self) -> Markup {
html! {
(DOCTYPE)
meta charset="utf-8";
html {
head {
title { (self.first_name.0) " " (self.last_name.0) " filled out a teacher reference form for " (self.student_first_name.0) " " (self.student_last_name.0) "!" }
style {
"table { border-collapse: collapse; width: 100% }"
"td, th { padding: 8px }"
"td { text-align: left; width: 70%; word-wrap: break-word }"
"th { text-align: right; border-right: 1px solid #ddd }"
"tr { border-bottom: 1px solid #ddd }"
"h1 { text-align: center }"
}
}
body {
h1 { "Teacher reference form for " (self.student_first_name.0) " " (self.student_last_name.0) "!" }
hr;
table {
tr {
th { "Name" }
td { (self.first_name.0) " " (self.last_name.0) }
}
tr {
th { "Student" }
td { (self.student_first_name.0) " " (self.student_last_name.0) }
}
tr {
th { "Relationship with student" }
td { (self.relationship.0) }
}
tr {
th { "Positive characteristics" }
td { (self.positive.0) }
}
tr {
th { "Negative characteristics" }
td { (self.negative.0) }
}
tr {
th { "Attitudes" }
td { (self.attitudes.0) }
}
tr {
th { "Teamwork" }
td { (self.challenges.0) }
}
tr {
th { "Behavior in school" }
td { (self.behavior.0) }
}
tr {
th { "Other Relevant Info" }
td { (self.extra_info.0) }
}
}
}
}
}
}
async fn store_form(&self) -> Result<()> {
let client = Client::new();
let map = HashMap::from(self);
let mut json = HashMap::new();
json.insert("data", map);
let link = r#"https://staff.tfcconnection.org/apps/tables/#/table/12/row/757"#;
let res = client
.post("https://staff.tfcconnection.org/ocs/v2.php/apps/tables/api/2/tables/12/rows")
.basic_auth("chris", Some("2VHeGxeC^Zf9KqFK^G@Pt!zu2q^6@b"))
.header("OCS-APIRequest", "true")
.header("Content-Type", "application/json")
.json(&json)
.send()
.await?;
if res.status().is_success() {
let res = res.text().await.unwrap();
Ok(())
} else {
Err(eyre!(
"Problem in storing data: {:?}",
res.error_for_status()
))
}
}
async fn send_email(&mut self) -> Result<()> {
let first = self.student_first_name.clone();
let last = self.student_last_name.clone();
let email_subject = format!("Teacher reference form for {} {}!", first, last);
info!("{first} {last} teacher reference form!");
let email = self.build_email().await;
let email = SinglePart::html(email.into_string());
if let Ok(m) = Message::builder()
.from(
"TFC ADMIN <no-reply@mail.tfcconnection.org>"
.parse()
.unwrap(),
)
.to("Chris Cochrun <chris@tfcconnection.org>".parse().unwrap())
.to("Ethan Rose <ethan@tfcconnection.org>".parse().unwrap())
.subject(email_subject)
.singlepart(email)
{
crate::email::send_email(m).await
} else {
Err(eyre!("Email incorrect"))
}
}
}
#[post("/api/mt-teacher-form")]
pub async fn mt_teacher_form(
MultipartForm(mut form): MultipartForm<MtTeacherForm>,
) -> HttpResponse {
match form.store_form().await {
Ok(_) => info!("Successfully sent form to nextcloud!"),
Err(e) => error!("There was an erroring sending form to nextcloud: {e}"),
}
match form.send_email().await {
Ok(_) => info!("Successfully sent email"),
Err(e) => error!("There was an error sending the email: {e}"),
}
HttpResponse::Ok().body("thankyou")
}
#[cfg(test)]
mod test {
use actix_web::test;
use pretty_assertions::assert_eq;
use sqlx::Connection;
use tracing::debug;
use super::*;
fn form() -> MtTeacherForm {
MtTeacherForm {
first_name: Text(String::from("Bilbo")),
last_name: Text(String::from("Braggins")),
student_first_name: Text(String::from("Frodo")),
student_last_name: Text(String::from("Braggins")),
relationship: Text(String::from("Uncle")),
positive: Text(String::from("Nimble and brave")),
negative: Text(String::from("Small")),
attitudes: Text(String::from("Lighthearted")),
challenges: Text(String::from("Willing")),
behavior: Text(String::from("Good")),
extra_info: Text(String::from("Willing to take the ring")),
}
}
#[test]
async fn test_nc_post() {
let form = form();
assert!(!form.first_name.is_empty());
let res = form.store_form().await;
match res {
Ok(_) => assert!(true, "passed storing test"),
Err(e) => assert!(false, "Failed storing test: {e}"),
}
}
#[test]
async fn test_email() {
let mut form = form();
assert!(!form.first_name.is_empty());
match form.send_email().await {
Ok(_) => assert!(true, "passed emailing test"),
Err(e) => assert!(false, "Failed emailing test: {e}"),
}
}
}

30
src/email.rs Normal file
View file

@ -0,0 +1,30 @@
use color_eyre::Result;
use lettre::{
transport::smtp::authentication::{Credentials, Mechanism},
Message, SmtpTransport, Transport,
};
use tracing::{error, info};
pub async fn send_email(message: Message) -> Result<()> {
let sender = SmtpTransport::relay("mail.tfcconnection.org")
.ok()
.unwrap()
.credentials(Credentials::new(
"no-reply@mail.tfcconnection.org".to_owned(),
"r9f36mNZFtiW4f".to_owned(),
))
.authentication(vec![Mechanism::Plain])
.build();
match sender.send(&message) {
Ok(res) => {
let res: String = res.message().collect();
info!(
"Successfully sent email to server with this response: {:?}",
res
)
}
Err(e) => error!("There was an error sending the email: {e}"),
}
Ok(())
}

View file

@ -1,37 +1,105 @@
mod api;
pub mod email;
use actix_files::Files;
use actix_multipart::form::tempfile::TempFileConfig;
use actix_web::{middleware, App, HttpServer};
use actix_web::body::MessageBody;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::{web, App, Error, HttpServer};
use api::camp_form::camp_form;
use api::church_form::church_form;
use api::contact::{self, contact_form};
use api::health_form::health_form;
use api::local_trip_form::local_form;
use api::mt_form::mt_form;
use api::parent_form::parent_form;
use api::teacher_form::teacher_form;
use api::{
mt_church_form::mt_church_form, mt_parent_form::mt_parent_form,
mt_teacher_form::mt_teacher_form,
};
use color_eyre::eyre::Context;
use color_eyre::Result;
use sqlx::{Connection, SqliteConnection};
use tracing::level_filters::LevelFilter;
use tracing::{info, Span};
use tracing_actix_web::{RootSpanBuilder, TracingLogger};
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Layer};
pub struct DomainRootSpanBuilder;
impl RootSpanBuilder for DomainRootSpanBuilder {
fn on_request_start(request: &ServiceRequest) -> Span {
let method = request.method();
let info = request.connection_info();
let ip = info.realip_remote_addr().expect("hi");
let location = request.path();
info!(?method, ip, location);
tracing_actix_web::root_span!(request)
// let client_id: &str = todo!("Somehow extract it from the authorization header");
}
fn on_request_end<B: MessageBody>(_span: Span, _outcome: &Result<ServiceResponse<B>, Error>) {}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let timer =
tracing_subscriber::fmt::time::ChronoLocal::new("%Y-%m-%d_%I:%M:%S%.6f %P".to_owned());
let logfile = RollingFileAppender::builder()
.rotation(Rotation::DAILY)
.filename_prefix("api")
.filename_suffix("log")
.build("/tmp/tfcsite")
.expect("Shouldn't");
log::info!("creating temporary upload directory");
std::fs::create_dir_all("./tmp")?;
let filter = EnvFilter::builder()
.with_default_directive(LevelFilter::WARN.into())
.parse_lossy("tfcapi=debug");
let logfile_layer = tracing_subscriber::fmt::layer()
.with_writer(logfile)
.with_line_number(true)
.with_level(true)
.with_target(true)
.with_ansi(false)
.with_timer(timer.clone());
let stdout_layer = tracing_subscriber::fmt::layer()
.pretty()
.with_line_number(true)
.with_target(true)
.with_timer(timer)
.with_filter(filter);
let filter = EnvFilter::builder()
.with_default_directive(LevelFilter::WARN.into())
.parse_lossy("tfcapi=debug");
let subscriber = tracing_subscriber::registry()
.with(logfile_layer.with_filter(filter).and_then(stdout_layer));
let _ = tracing::subscriber::set_global_default(subscriber).wrap_err("Tracing broked");
log::info!("starting HTTP server at http://localhost:4242");
std::fs::create_dir_all("/tmp/tfcsite")?;
HttpServer::new(|| {
info!("starting HTTP server at http://localhost:4242");
let conn = SqliteConnection::connect("sqlite://./data.db")
.await
.expect("Couldn't connect sqlite db");
let data = web::Data::new(conn);
HttpServer::new(move || {
App::new()
.wrap(middleware::Logger::default())
.app_data(TempFileConfig::default().directory("./tmp"))
.app_data(data.clone())
.wrap(TracingLogger::<DomainRootSpanBuilder>::new())
.app_data(TempFileConfig::default().directory("/tmp/tfcsite"))
.service(mt_form)
.service(health_form)
.service(parent_form)
.service(teacher_form)
.service(church_form)
.service(mt_parent_form)
.service(mt_teacher_form)
.service(mt_church_form)
.service(local_form)
.service(camp_form)
.service(contact_form)
.service(Files::new("/", "./public").index_file("index.html"))
})
.bind(("127.0.0.1", 4242))?
.bind(("localhost", 4242))?
.workers(2)
.run()
.await

2
src/testing.json Normal file

File diff suppressed because one or more lines are too long

View file

@ -998,6 +998,10 @@
--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-1 {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@ -1016,6 +1020,9 @@
--tw-blur: blur(8px);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));

View file

@ -4,35 +4,35 @@
<div class="m-4 w-full grid grid-cols-1 gap-12 lg:grid-cols-4 md:grid-cols-2 sm:grid-cols-1">
<div class="p-2">
<div class="text-xl font-extrabold">Lost and Seeking</div>
<div class="text-2xl font-extrabold">Lost and Seeking</div>
<ul>
<li>The World</li>
<li>Outreach</li>
<li>Relationship Building</li>
<li class="py-2">The World</li>
<li class="py-2">Outreach</li>
<li class="py-2">Relationship Building</li>
</ul>
</div>
<div class="p-2">
<div class="text-xl font-extrabold">Come and Grow</div>
<div class="text-2xl font-extrabold">Come and Grow</div>
<ul>
<li>Growing Believers</li>
<li>Building Faith</li>
<li>Jesus Started Here</li>
<li class="py-2">Growing Believers</li>
<li class="py-2">Building Faith</li>
<li class="py-2">Jesus Started Here</li>
</ul>
</div>
<div class="p-2">
<div class="text-xl font-extrabold">Come and Follow</div>
<div class="text-2xl font-extrabold">Come and Follow</div>
<ul>
<li>Workers</li>
<li>Equipping</li>
<li>Jesus Invited Some</li>
<li class="py-2">Workers</li>
<li class="py-2">Equipping</li>
<li class="py-2">Jesus Invited Some</li>
</ul>
</div>
<div class="p-2">
<div class="text-xl font-extrabold">Come and Go</div>
<div class="text-2xl font-extrabold">Come and Go</div>
<ul>
<li>Leadership</li>
<li>Multiplication</li>
<li>Jesus Sent Some</li>
<li class="py-2">Leadership</li>
<li class=py-2>Multiplication</li>
<li class=py-2>Jesus Sent Some</li>
</ul>
</div>
</div>