parent
c7523bf117
commit
c1f1be0d2d
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>
|
||||
© 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 — Compiler Design: {{ interest_compilers }} interested sutdents.</li>
|
||||
<li>CPSC4780 — 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…
Reference in new issue