Initial commit

main
Tait Hoyem 4 months ago
parent c7523bf117
commit c1f1be0d2d

2360
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,16 @@
[package]
name = "forms-tait-tech"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
askama = { version = "0.12.1", features = ["with-axum"] }
askama_axum = "0.4.0"
axum = "0.7.2"
axum_csrf = { version = "0.9.0", features = ["layer"] }
lettre = { version = "0.11.2", default-features = false, features = ["tokio1-rustls-tls", "tokio1", "smtp-transport", "builder"] }
serde = "1.0.193"
sqlx = { version = "0.7.3", features = ["postgres", "runtime-tokio"] }
tokio = { version = "1.35.0", features = ["macros", "rt-multi-thread"] }

@ -0,0 +1,3 @@
-- Add down migration script here
DROP TABLE IF EXISTS responses;
DROP TYPE IF EXISTS campus;

@ -0,0 +1,13 @@
-- Add up migration script here
CREATE TYPE campus AS ENUM ('c', 'l');
CREATE TABLE IF NOT EXISTS responses (
id SERIAL NOT NULL,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
uleth_id INTEGER NOT NULL,
compiler_design BOOLEAN NOT NULL,
distributed_systems BOOLEAN NOT NULL,
comments VARCHAR(1000) NOT NULL,
school_campus campus NOT NULL
);

@ -0,0 +1,13 @@
use serde::de;
pub fn deserialize_checkbox<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: de::Deserializer<'de>,
{
let s: &str = de::Deserialize::deserialize(deserializer)?;
match s {
"on" => Ok(true),
_ => Ok(false),
}
}

@ -0,0 +1,230 @@
mod checkbox;
use std::env;
use std::sync::Arc;
use lettre::message::{header::ContentType, Message, Mailbox, MultiPart};
use lettre::transport::{smtp::authentication::Mechanism, smtp::authentication::Credentials};
use lettre::transport::smtp::AsyncSmtpTransport;
use lettre::AsyncTransport;
use serde::{Serialize, Deserialize};
use askama::{Template};
use sqlx::{FromRow, query_as, postgres::{PgPool, PgQueryResult, PgPoolOptions}, query, query_scalar};
use axum::{Form, response::IntoResponse, routing::get, Router, body::Body, response::Response};
use axum::extract::State;
use axum_csrf::{CsrfConfig, CsrfLayer, CsrfToken};
#[derive(Template, Deserialize, Serialize)]
#[template(path = "course_interest.html")]
struct FormPage {
authentication_key: String,
interest_distirbuted: i64,
interest_compilers: i64,
}
#[derive(Debug, FromRow)]
struct DBFormResponse {
name: String,
email: String,
uleth_id: i32,
compiler_design: bool,
distributed_systems: bool,
comments: String,
}
impl From<HTMLFormResponse> for DBFormResponse {
fn from(html_form: HTMLFormResponse) -> Self {
DBFormResponse {
name: html_form.name,
email: html_form.email,
uleth_id: html_form.uleth_id,
compiler_design: html_form.compiler_design,
distributed_systems: html_form.distributed_systems,
comments: html_form.comments,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct HTMLFormResponse {
name: String,
email: String,
uleth_id: i32,
#[serde(default = "bool::default")]
#[serde(deserialize_with = "checkbox::deserialize_checkbox")]
compiler_design: bool,
#[serde(default = "bool::default")]
#[serde(deserialize_with = "checkbox::deserialize_checkbox")]
distributed_systems: bool,
comments: String,
authentication_key: String,
}
struct Interest {
distributed_systems: Option<i64>,
compiler_design: Option<i64>,
}
impl DBFormResponse {
async fn insert(pool: &PgPool, to_add: &Self) -> Result<PgQueryResult, sqlx::Error> {
query!(
r#"
INSERT INTO responses
(name,email,uleth_id,compiler_design,distributed_systems,comments)
VALUES
($1, $2, $3, $4, $5, $6)
"#,
to_add.name,
to_add.email,
to_add.uleth_id,
to_add.compiler_design,
to_add.distributed_systems,
to_add.comments)
.execute(&*pool)
.await
}
async fn all(pool: &PgPool) -> Result<Vec<DBFormResponse>, sqlx::Error> {
query_as!(DBFormResponse,
r#"
SELECT name,email,uleth_id,compiler_design,distributed_systems,comments
FROM responses;
"#)
.fetch_all(&*pool)
.await
}
async fn count(pool: &PgPool) -> Result<Option<i64>, sqlx::Error> {
query_scalar!(
r#"
SELECT COUNT(id)
FROM responses;
"#)
.fetch_one(&*pool)
.await
}
async fn interest_counts(pool: &PgPool) -> Result<Interest, sqlx::Error> {
query_as!(Interest,
r#"
SELECT
SUM(CASE WHEN distributed_systems = true THEN 1 ELSE 0 END) AS distributed_systems,
SUM(CASE WHEN compiler_design = true THEN 1 ELSE 0 END) AS compiler_design
FROM responses;
"#)
.fetch_one(&*pool)
.await
}
}
async fn index(
token: CsrfToken,
State(ServerState { db }): State<ServerState>,
) -> Result<(CsrfToken, FormPage), FormError> {
let responses_already = DBFormResponse::interest_counts(&db).await?;
let tmpl = FormPage {
authentication_key: token.authenticity_token().unwrap(),
interest_distirbuted: responses_already.distributed_systems.unwrap_or(0i64),
interest_compilers: responses_already.compiler_design.unwrap_or(0i64),
};
Ok((token, tmpl))
}
#[derive(Debug, Serialize, Deserialize)]
pub enum FormError {
EmailError(String),
DatabaseError(String),
}
impl IntoResponse for FormError {
fn into_response(self) -> Response<Body> {
match self {
Self::EmailError(s) => {
eprintln!("{}", s);
"There was an error sending the email to your inbox."
},
Self::DatabaseError(s) => {
eprintln!("{}", s);
"There was an error saving your response to the database."
}
}.to_string().into_response()
}
}
impl From<lettre::address::AddressError> for FormError {
fn from(e: lettre::address::AddressError) -> Self {
Self::EmailError(format!("{:?}", e))
}
}
impl From<lettre::transport::smtp::Error> for FormError {
fn from(e: lettre::transport::smtp::Error) -> Self {
Self::EmailError(format!("{:?}", e))
}
}
impl From<lettre::error::Error> for FormError {
fn from(e: lettre::error::Error) -> Self {
Self::EmailError(format!("{:?}", e))
}
}
impl From<sqlx::Error> for FormError {
fn from(e: sqlx::Error) -> Self {
Self::DatabaseError(format!("{:?}", e))
}
}
async fn check_key(
token: CsrfToken,
State(ServerState { db }): State<ServerState>,
Form(payload): Form<HTMLFormResponse>,
) -> Result<String, FormError> {
// Verfiy the Hash and return the String message.
if token.verify(&payload.authentication_key).is_err() {
return Ok("CSRF token is invalid. This is usually because somebody is doing something nasty.".to_string());
}
let db_form_resposne = payload.clone().into();
let _ = DBFormResponse::insert(&db, &db_form_resposne).await?;
let smtp_host = env::var("SMTP_HOST").unwrap();
let smtp_user = env::var("SMTP_USER").unwrap();
let smtp_pass = env::var("SMTP_PASS").unwrap();
let msg = Message::builder()
.from("Tait Hoyem <tait@tait.tech>".parse()?)
.to(Mailbox::new(Some(payload.name.clone()), payload.email.clone().parse()?))
.bcc("Tait Hoyem <tait.hoyem@uleth.ca>".parse()?)
.subject("Thank Your For Your Interest")
.multipart(MultiPart::alternative_plain_html(
String::from("Test"),
String::from("<p>What is the secondPart?</p>"),
))?;
let sender = AsyncSmtpTransport::<lettre::Tokio1Executor>::starttls_relay(&smtp_host)?
.credentials(Credentials::new(
smtp_user,
smtp_pass,
))
.authentication(vec![Mechanism::Login])
.build();
let result = sender.send(msg).await;
Ok(format!("{:?}", payload))
}
#[derive(Clone)]
struct ServerState {
db: Arc<PgPool>,
}
async fn db_connect() -> PgPool {
PgPoolOptions::new()
.max_connections(8)
.connect(&env::var("DATABASE_URL").unwrap())
.await
.unwrap()
}
#[tokio::main]
async fn main() {
let config = CsrfConfig::default();
let db = db_connect().await;
let state = ServerState {
db: Arc::new(db),
};
let app = Router::new()
.route("/", get(index).post(check_key))
.layer(CsrfLayer::new(config))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}

@ -0,0 +1,75 @@
body { background-color: #222222; padding: 16px; font-family: -apple-system, helvetica, arial, sans-serif; font-size: 16px; color: #ffffff; line-height: 1.5em; overflow-wrap: break-word; }
#wrapper { max-width: 800px; margin: auto; }
#wrapper > header { text-align: center; }
.clear-list { list-style-type: none; margin: 0; padding: 0; }
.clear-list li { margin: 0; padding: 0; }
.projects .namelink { font-weight: bold; }
h1, h2, h3, h4, h5, h6 { line-height: 1em; }
.center { text-align: center; }
header { margin-bottom: 18px; }
hr { border: none; border-bottom: 1px solid #999; }
a { text-decoration: underline; color: #7ad; }
a:visited { color: #ff3492; }
a.nav-link, a.post-title-link { color: #ffffff; text-decoration: none; }
.post-title { line-height: 1.5em; color: #ffffff; margin-bottom: 8px; }
a.citation-link { text-decoration: none; }
nav { text-align: center; padding: 1em 0px; }
nav a { margin: 1em; color: #ffffff; font-weight: bold; font-style: none; }
nav a:hover, a.post-title-link:hover { text-decoration: underline; }
li { margin: .5em; }
table, table tr, table td, table th { border: 1px solid #aaa; border-collapse: collapse; padding: 5px; font-weight: normal; }
table th { font-weight: bold; }
table { width: 75%; margin: auto; }
img { display: block; width: 55%; margin-left: auto; margin-right: auto; }
blockquote { font-style: italic; }
address { font-style: normal; }
@media screen and (max-width: 600px) { body { width: 90%; } nav { text-align: left; width: 100%; } nav a { display: block; text-align: left; padding-left: 0; margin-left: 0; } }
.mono { font-family: monospace; }
.bold { font-weight: bold; }
figcaption { margin-top: 10px; font-style: italic; }
footer { padding-top: 16px; margin-bottom: 100px; }
.terminal, .file { padding: 10px; overflow-x: scroll; }
.terminal { line-height: 1em; color: #00FF00; background-color: #000000; }
.file { line-height: 1.2em; background-color: #444444; color: #ffffff; }
.small-image { width: 100%; }
.post-date { text-transform: uppercase; font-weight: bold; color: #ffffff; }
.post-excerpt { margin-left: 24px; }
.post-header { margin-bottom: 8px; }
/*# sourceMappingURL=style.css.map */

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %} | forms.tait.tech</title>
{% block head %}{% endblock %}
</head>
<body>
<main>
{% block content %}{% endblock %}
</main>
<footer>
&copy; Tait Hoyem, 2023
</footer>
</body>
</html>

@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block title %}Cousre Interest Registration{% endblock %}
{% block content %}
<p>Here is the breakdown of interest for these courses:</p>
<ul>
<li>CPSC4600 &#x2014; Compiler Design: {{ interest_compilers }} interested sutdents.</li>
<li>CPSC4780 &#x2014; Distributed Systems: {{ interest_distirbuted }} interested students.</li>
</ul>
<form method="POST">
<fieldset>
<legend>Personal/School Information</legend>
<label for="name">Name</label>
<input type="text" id="name" name="name"/>
<label for="email">Email</label>
<input type="email" id="email" name="email"/>
<label for="id">ULeth ID#</label>
<input type="id" id="id" name="uleth_id"/>
<label for="camp">Campus</label>
<select id="camp" name="campus">
<option value="c">Calgary</option>
<option value="l">Lethbridge</option>
</select>
</fieldset>
<fieldset>
<legend>Interested Topics</legend>
<label for="ds">Distributed Systems</label>
<input type="checkbox" id="ds" name="distributed_systems"/>
<label for="cd">Distributed Systems</label>
<input type="checkbox" id="cd" name="compiler_design"/>
<label for="ac">Additional Comments</label>
<textarea id="ac" name="comments">
</textarea>
</fieldset>
<input type="submit" value="Register Interest"/>
<input type="hidden" name="authentication_key" value="{{ authentication_key }}"/>
</form>
{% endblock %}
Loading…
Cancel
Save