Skip to content

Commit 5e52db5

Browse files
committed
feat: add cusom email verification
1 parent ecba49d commit 5e52db5

File tree

10 files changed

+104
-56
lines changed

10 files changed

+104
-56
lines changed

api/Cargo.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ hex = "0.4.3"
2727
http-body-util = "0.1.3"
2828
regex = "1.11.2"
2929
cached = { version = "0.56.0", features = ["async"] }
30+
hmac = "0.12.1"
31+
sha2 = "0.10.9"
32+
jwt = "0.16.0"

api/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ mod meta;
2121
pub struct AppState {
2222
dynamo: aws_sdk_dynamodb::Client,
2323
scheduler: aws_sdk_scheduler::Client,
24+
ses: aws_sdk_sesv2::Client,
2425
discord_bot: Arc<twilight_http::Client>,
2526
reqwest: Arc<reqwest::Client>,
2627
}
@@ -72,6 +73,7 @@ async fn main() -> Result<(), Error> {
7273
let app_state = AppState {
7374
dynamo: aws_sdk_dynamodb::Client::new(&config),
7475
scheduler: aws_sdk_scheduler::Client::new(&config),
76+
ses: aws_sdk_sesv2::Client::new(&config),
7577
discord_bot,
7678
reqwest: Arc::new(reqwest::Client::new()),
7779
};

api/src/users/email.rs

Lines changed: 33 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,49 @@
1-
use aws_sdk_sesv2::{Client, Error};
2-
use aws_sdk_sesv2::types::{Body, Content, Destination, EmailContent, Message, Template};
1+
use aws_sdk_sesv2::Client;
2+
use aws_sdk_sesv2::types::{Destination, EmailContent, Template};
3+
use http::StatusCode;
4+
use serde::Serialize;
5+
use crate::discord::ise;
6+
7+
#[derive(Serialize)]
8+
struct TemplateData {
9+
name: String,
10+
link_url: String
11+
}
312

4-
async fn send_message(
13+
pub async fn send_verify_email(
514
client: &Client,
6-
list: &str,
7-
from: &str,
8-
subject: &str,
9-
message: &str,
10-
) -> Result<(), Error> {
11-
// Get list of email addresses from contact list.
12-
let resp = client
13-
.list_contacts()
14-
.contact_list_name(list)
15-
.send()
16-
.await?;
17-
18-
let contacts = resp.contacts();
19-
20-
let cs: Vec<String> = contacts
21-
.iter()
22-
.map(|i| i.email_address().unwrap_or_default().to_string())
23-
.collect();
24-
25-
let mut dest: Destination = Destination::builder().build();
26-
dest.to_addresses = Some(cs);
27-
let subject_content = Content::builder()
28-
.data(subject)
29-
.charset("UTF-8")
30-
.build()
31-
.expect("building Content");
32-
let body_content = Content::builder()
33-
.data(message)
34-
.charset("UTF-8")
35-
.build()
36-
.expect("building Content");
37-
let body = Body::builder().text(body_content).build();
38-
39-
let msg = Message::builder()
40-
.subject(subject_content)
41-
.body(body)
42-
.build();
43-
44-
let coupons = std::fs::read_to_string("../resources/newsletter/sample_coupons.json")
45-
.unwrap_or_else(|_| r#"{"coupons":[]}"#.to_string());
15+
global_name: &str,
16+
email_addr: String,
17+
token: &str,
18+
) -> Result<(), StatusCode> {
19+
let env = std::env::var("DEPLOYMENT_ENV").expect("DEPLOYMENT_ENV must be set");
20+
let mut host = "koalabot.uk".to_string();
21+
if env != "prod" {
22+
host = format!("{}.{}", env, host);
23+
}
24+
25+
let t_data = serde_json::to_string(&TemplateData {
26+
name: global_name.to_string(),
27+
link_url: format!("https://{host}/verify/email/callback?token={token}").to_string(),
28+
}).map_err(ise)?;
29+
4630
let email_content = EmailContent::builder()
4731
.template(
4832
Template::builder()
49-
.template_name("kb2-verify-template-") //fixme
50-
.template_data(coupons)
33+
.template_name(format!("kb2-verify-template-{env}"))
34+
.template_data(t_data)
5135
.build(),
5236
)
5337
.build();
5438

5539
client
5640
.send_email()
57-
.from_email_address(from)
58-
.destination(dest)
41+
.from_email_address(format!("no-reply@{host}"))
42+
.destination(Destination::builder().to_addresses(email_addr).build())
5943
.content(email_content)
6044
.send()
61-
.await?;
62-
63-
println!("Email sent to list");
45+
.await
46+
.map_err(ise)?;
6447

6548
Ok(())
6649
}

api/src/users/links.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::BTreeMap;
12
use crate::AppState;
23
use crate::users::models::{Link, User};
34
use axum::extract::{Path, State};
@@ -7,15 +8,21 @@ use lambda_http::tracing::info;
78
use serde::{Deserialize, Serialize};
89
use serde_json::json;
910
use std::sync::Arc;
11+
use hmac::{Hmac, Mac};
12+
use jwt::{SignWithKey, VerifyWithKey};
13+
use sha2::Sha256;
1014
use tower_http::cors::CorsLayer;
1115
use twilight_model::id::Id;
1216
use twilight_model::id::marker::UserMarker;
1317
use twilight_model::user::CurrentUser;
18+
use crate::discord::ise;
1419
use crate::guilds::models::Guild;
20+
use crate::users::email::send_verify_email;
1521

1622
pub fn router() -> axum::Router<AppState> {
1723
axum::Router::new()
1824
.route("/", post(post_link))
25+
.route("/send-email", post(post_send_email))
1926
.route("/{link_address}", delete(delete_link))
2027
.layer(CorsLayer::permissive())
2128
}
@@ -69,7 +76,13 @@ async fn post_link(
6976
.await?
7077
}
7178
LinkOrigin::Email => {
72-
todo!();
79+
let key: Hmac<Sha256> = Hmac::new_from_slice(std::env::var("DISCORD_BOT_TOKEN").expect("DISCORD_BOT_TOKEN must be set").into_bytes().as_ref())
80+
.map_err(ise)?;
81+
let claims: BTreeMap<String, String> = link_req.token.verify_with_key(&key).map_err(ise)?;
82+
if claims.get("exp").unwrap().parse::<u64>().unwrap() < chrono::Utc::now().timestamp() as u64 {
83+
return Err(http::StatusCode::UNAUTHORIZED);
84+
}
85+
claims.get("sub").unwrap().to_string()
7386
}
7487
};
7588

@@ -170,3 +183,26 @@ async fn delete_link(
170183
json!({"status": "success", "message": "Link deleted"}),
171184
))
172185
}
186+
187+
#[derive(Clone, Serialize, Deserialize)]
188+
struct SendEmailRequest {
189+
email: String,
190+
}
191+
192+
async fn post_send_email(
193+
// Path(_user_id): Path<Id<UserMarker>>,
194+
Extension(current_user): Extension<CurrentUser>,
195+
State(app_state): State<AppState>,
196+
Json(send_email_req): Json<SendEmailRequest>,
197+
) -> Result<Json<serde_json::Value>, http::StatusCode> {
198+
let key: Hmac<Sha256> = Hmac::new_from_slice(std::env::var("DISCORD_BOT_TOKEN").expect("DISCORD_BOT_TOKEN must be set").into_bytes().as_ref())
199+
.map_err(ise)?;
200+
let mut claims = BTreeMap::new();
201+
claims.insert("sub", send_email_req.email.clone());
202+
claims.insert("exp", (chrono::Utc::now() + chrono::Duration::hours(1)).timestamp().to_string());
203+
let token_str = claims.sign_with_key(&key).map_err(ise)?;
204+
205+
send_verify_email(&app_state.ses, &*current_user.name, send_email_req.email, &token_str).await?;
206+
207+
Ok(Json(json!({"status": "success"})))
208+
}

api/src/users/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
mod links;
22
pub mod models;
3-
mod email;
3+
pub mod email;
44

55
use std::sync::Arc;
66
use crate::AppState;

infra/modules/data/ses/main.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ data "local_file" "verify_template_txt" {
88
}
99

1010
resource "aws_ses_template" "verify_template" {
11-
name = "kb2-verify-templacte-${var.deployment_env}"
11+
name = "kb2-verify-template-${var.deployment_env}"
1212
subject = "Verify your email with Koala!"
1313
html = data.local_file.verify_template_html.content
1414
text = data.local_file.verify_template_txt.content

infra/modules/data/ses/resources/email-template.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
111111
<tbody>
112112
<tr>
113-
<td style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top; border-radius: 4px; text-align: center; background-color: #0867ec;" valign="top" align="center" bgcolor="#0867ec"> <a href="{{linkUrl}}" target="_blank" style="border: solid 2px #0867ec; border-radius: 4px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 16px; font-weight: bold; margin: 0; padding: 12px 24px; text-decoration: none; text-transform: capitalize; background-color: #0867ec; border-color: #0867ec; color: #ffffff;">Link Account</a> </td>
113+
<td style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top; border-radius: 4px; text-align: center; background-color: #0867ec;" valign="top" align="center" bgcolor="#0867ec"> <a href="{{link_url}}" target="_blank" style="border: solid 2px #0867ec; border-radius: 4px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 16px; font-weight: bold; margin: 0; padding: 12px 24px; text-decoration: none; text-transform: capitalize; background-color: #0867ec; border-color: #0867ec; color: #ffffff;">Link Account</a> </td>
114114
</tr>
115115
</tbody>
116116
</table>

infra/modules/data/ses/resources/email-template.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Hi {{name}},
22
Someone has requested to link their Discord account to this email address. If this was you, please confirm by clicking the link below.
3-
{{linkUrl}}
3+
{{link_url}}
44

55
If this was not you, please ignore this email, or if you are having further issues, please contact our support team.
66

ui/src/components/verify/EmailAuthText.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {User} from "../../stores/user.js";
77
88
const KB_API_URL = import.meta.env.VITE_KB_API_URL
99
10-
let todo = true;
10+
let todo = false;
1111
let emailInput = defineModel();
1212
let disableTextField = ref(false);
1313

0 commit comments

Comments
 (0)