Skip to content

Commit

Permalink
Merge pull request #16 from pesca-dev/verification-mail
Browse files Browse the repository at this point in the history
Feature: Verification mail
  • Loading branch information
H1ghBre4k3r authored Sep 1, 2023
2 parents b009634 + b677baf commit 3e9b180
Show file tree
Hide file tree
Showing 12 changed files with 557 additions and 11 deletions.
281 changes: 278 additions & 3 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ chrono = "0.4.28"
surrealdb = { git = "https://github.com/pesca-dev/surrealdb.git", tag = "v1.0.0-beta.9+20230402", optional = true }
tracing = "0.1.37"
tracing-subscriber = "0.3.17"
lettre = { version = "0.10.4", optional = true }
jwt = { version = "0.16.0", optional = true }
hmac = { version = "0.12.1", optional = true }
sha2 = { version = "0.10.7", optional = true }

[features]
# default = ["csr"]
Expand All @@ -40,6 +44,10 @@ ssr = [
"dep:leptos_actix",
"dep:argon2",
"dep:surrealdb",
"dep:lettre",
"dep:jwt",
"dep:hmac",
"dep:sha2",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
Expand Down
3 changes: 2 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
contexts::AuthContextProvider,
views::{
CodeView, HomeView, ImpressumView, LoginView, LogoutView, ProfileView, RegisterView,
SettingsView,
SettingsView, VerifyView,
},
};

Expand Down Expand Up @@ -37,6 +37,7 @@ pub fn App(cx: Scope) -> impl IntoView {
<Route path="/settings" view=SettingsView ssr=SsrMode::Async/>
<Route path="/login" view=LoginView ssr=SsrMode::Async/>
<Route path="/register" view=RegisterView ssr=SsrMode::Async/>
<Route path="/verify" view=VerifyView ssr=SsrMode::Async/>
<Route path="/logout" view=LogoutView ssr=SsrMode::Async/>
<Route path="/code" view=CodeView ssr=SsrMode::Async/>
<Route path="/code/:user" view=CodeView ssr=SsrMode::Async/>
Expand Down
11 changes: 10 additions & 1 deletion src/contexts/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use cfg_if::cfg_if;

use leptos::*;

use crate::functions::{Login, LoginResult, Logout, Register, RegistrationResult};
use crate::functions::{
Login, LoginResult, Logout, Register, RegistrationResult, ResendVerification,
VerificationResult, Verify,
};

cfg_if! {
if #[cfg(feature = "ssr")] {
Expand All @@ -15,6 +18,8 @@ pub struct AuthContext {
pub login: Action<Login, Result<LoginResult, ServerFnError>>,
pub register: Action<Register, Result<RegistrationResult, ServerFnError>>,
pub logout: Action<Logout, Result<(), ServerFnError>>,
pub verify: Action<Verify, Result<VerificationResult, ServerFnError>>,
pub resend_verification_email: Action<ResendVerification, Result<(), ServerFnError>>,
pub user: Resource<(usize, usize, usize), Result<Option<String>, ServerFnError>>,
}

Expand All @@ -23,6 +28,8 @@ impl AuthContext {
let login = create_server_action::<Login>(cx);
let logout = create_server_action::<Logout>(cx);
let register = create_server_action::<Register>(cx);
let verify = create_server_action::<Verify>(cx);
let resend_verification_email = create_server_action::<ResendVerification>(cx);

let user = create_resource(
cx,
Expand All @@ -40,6 +47,8 @@ impl AuthContext {
login,
logout,
register,
verify,
resend_verification_email,
user,
}
}
Expand Down
89 changes: 86 additions & 3 deletions src/functions/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,30 @@ use serde::{Deserialize, Serialize};

cfg_if! {
if #[cfg(feature = "ssr")] {
use std::error::Error;
use std::collections::BTreeMap;
use actix_identity::IdentityExt;
use crate::hooks::use_identity;
use crate::utils::password::hash_password;
use crate::model::{User, LoginError, Session};
use crate::services::{mail::Mail, jwt};

fn create_jwt(username: &str) -> Result<String, Box<dyn Error>> {
let mut claims = BTreeMap::new();
claims.insert("sub".into(), username.to_string());
jwt::sign(claims)
}

fn send_verification_mail(username: String, email: String, token: String) -> Result<(), Box<dyn Error>> {

let mail = Mail {
subject: Some("Registration Mail".into()),
recipient: email,
content: Some(format!("Hey {username}! \nThank you for registering! To complete your registration, please use the following link: https://aoc.inf-cau.de/verify?token={token}"))
};

mail.send()
}
}
}

Expand Down Expand Up @@ -47,10 +67,19 @@ pub async fn register(
return Ok(RegistrationResult::PasswordsDoNotMatch);
}

// create JWT for verification mail
let token = match create_jwt(&username) {
Ok(token) => token,
Err(e) => {
tracing::error!("failed to create JWT: {e:#?}");
return Ok(RegistrationResult::InternalServerError);
}
};

if let Err(e) = (User {
username,
username: username.clone(),
password: hash_password(password)?,
email,
email: email.clone(),
..Default::default()
})
.create()
Expand All @@ -62,6 +91,10 @@ pub async fn register(
return Ok(e);
};

if send_verification_mail(username, email, token).is_err() {
return Ok(RegistrationResult::InternalServerError);
}

Ok(RegistrationResult::Ok)
}

Expand All @@ -82,7 +115,7 @@ impl Display for LoginResult {
Ok => f.write_str("Login Successful"),
InternalServerError => f.write_str("Internal Server Error"),
WrongCredentials => f.write_str("Wrong Credentials"),
VerifyEmail => f.write_str("Verify your Email bevore logging in"),
VerifyEmail => f.write_str("Verify your Email before logging in"),
AlreadyLoggedIn => f.write_str("You are already logged in"),
}
}
Expand Down Expand Up @@ -138,3 +171,53 @@ pub async fn logout(cx: Scope) -> Result<(), ServerFnError> {

Ok(())
}

#[derive(Serialize, Deserialize)]
pub enum VerificationResult {
Ok,
InvalidToken,
InternalServerError,
}

#[server(Verify, "/api")]

Check warning on line 182 in src/functions/auth.rs

View workflow job for this annotation

GitHub Actions / Build & Test (nightly)

`&` without an explicit lifetime name cannot be used here

Check warning on line 182 in src/functions/auth.rs

View workflow job for this annotation

GitHub Actions / Build & Test (nightly)

`&` without an explicit lifetime name cannot be used here
pub async fn verify_user(cx: Scope, token: String) -> Result<VerificationResult, ServerFnError> {
let payload = match jwt::extract(token) {
Ok(data) => data,
Err(e) => {
tracing::warn!("failed to extract JWT: {e:#?}");
return Ok(VerificationResult::InternalServerError);
}
};

let username = match payload.get("sub") {
Some(username) => username,
None => {
return Ok(VerificationResult::InvalidToken);
}
};

let user = match User::get_by_username(username).await {
Some(user) => user,
None => {
return Ok(VerificationResult::InvalidToken);
}
};

user.verify_email().await;

Ok(VerificationResult::Ok)
}

#[server(ResendVerification, "/api")]

Check warning on line 211 in src/functions/auth.rs

View workflow job for this annotation

GitHub Actions / Build & Test (nightly)

`&` without an explicit lifetime name cannot be used here

Check warning on line 211 in src/functions/auth.rs

View workflow job for this annotation

GitHub Actions / Build & Test (nightly)

`&` without an explicit lifetime name cannot be used here
pub async fn resend_verification_mail(cx: Scope, username: String) -> Result<(), ServerFnError> {
let Some(user) = User::get_by_username(&username).await else {
return Ok(());
};

let Ok(token) = create_jwt(&username) else {
return Ok(());
};

let _ = send_verification_mail(username, user.email, token);
Ok(())
}
54 changes: 54 additions & 0 deletions src/services/jwt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use std::collections::BTreeMap;
use std::env;
use std::error::Error;

use hmac::digest::KeyInit;
use hmac::Hmac;
use jwt::{SignWithKey, VerifyWithKey};
use sha2::Sha256;

fn key() -> Result<Hmac<Sha256>, Box<dyn Error>> {
let key = env::var("JWT_KEY").expect("JWT key should be given");
Ok(Hmac::new_from_slice(key.as_bytes())?)
}

pub fn sign(claims: BTreeMap<String, String>) -> Result<String, Box<dyn Error>> {
let key = key()?;

Ok(claims.sign_with_key(&key)?)
}

pub fn extract(token: String) -> Result<BTreeMap<String, String>, Box<dyn Error>> {
let key = key()?;

Ok(token.verify_with_key(&key)?)
}

#[cfg(test)]
mod tests {
use super::*;

use std::{collections::BTreeMap, env};

#[test]
fn test_jwt_sign() {
env::set_var("JWT_KEY", "some-key");

let mut claims = BTreeMap::new();
claims.insert("sub".to_string(), "some_user".to_string());
assert!(sign(claims).is_ok())
}

#[test]
fn test_jwt_extract() {
env::set_var("JWT_KEY", "some-key");

let mut claims = BTreeMap::new();
claims.insert("sub".to_string(), "some_user".to_string());

let token = sign(claims).unwrap();
let claims = extract(token).unwrap();

assert_eq!(claims["sub"], "some_user".to_string());
}
}
43 changes: 43 additions & 0 deletions src/services/mail.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::{env, error::Error};

use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
SmtpTransport, Transport,
};

pub struct Mail {
pub subject: Option<String>,
pub recipient: String,
pub content: Option<String>,
}

impl Mail {
pub fn send(self) -> Result<(), Box<dyn Error>> {
let mail_user = env::var("MAIL_USER").unwrap();
let mail_pass = env::var("MAIL_PASS").unwrap();
let mail_server = env::var("MAIL_SERVER").unwrap();
let mail_sender = env::var("MAIL_SENDER").unwrap();

let email = Message::builder()
.from(mail_sender.parse().unwrap())
.to(format!("<{}>", self.recipient).parse().unwrap())
.subject(self.subject.unwrap_or("".into()))
.header(ContentType::TEXT_PLAIN)
.body(self.content.unwrap_or("".into()))
.unwrap();

let creds = Credentials::new(mail_user, mail_pass);
let mailer = SmtpTransport::relay(&mail_server)
.unwrap()
.credentials(creds)
.build();

// Send the email
if let Err(e) = mailer.send(&email) {
tracing::error!("failed to send mail: {e:#?}");
return Err(Box::new(e));
}

Ok(())
}
}
2 changes: 2 additions & 0 deletions src/services/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
pub mod database;
pub mod mail;
pub mod jwt;
}
}
42 changes: 39 additions & 3 deletions src/views/login.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use leptos::*;
use leptos_router::ActionForm;

use crate::{functions::LoginResult, hooks::use_auth};
use crate::{
functions::{LoginResult, ResendVerification},
hooks::use_auth,
};

#[component]
pub fn LoginView(cx: Scope) -> impl IntoView {
Expand Down Expand Up @@ -30,6 +33,17 @@ pub fn LoginView(cx: Scope) -> impl IntoView {

let is_ok = move || matches!(result(), Some(LoginResult::Ok));

let need_to_verify_email = move || matches!(result(), Some(LoginResult::VerifyEmail));

let (username, set_username) = create_signal(cx, "".to_string());
let (password, set_password) = create_signal(cx, "".to_string());

let resend_verification_email = move |_| {
auth.resend_verification_email.dispatch(ResendVerification {
username: username(),
});
};

view! { cx,
<Transition
fallback=move || ()>
Expand All @@ -55,15 +69,37 @@ pub fn LoginView(cx: Scope) -> impl IntoView {
>
{message()}
</div>
<Show
when=need_to_verify_email
fallback=|_| view! {cx, <></>}>
<a
href="#"
on:click=resend_verification_email
>"Resend Email"</a>
</Show>
</Show>
<h1>"Login"</h1>
<label>
<span>"Username"</span>
<input type="text" name="username" required/>
<input
type="text"
name="username"
prop:value=username
on:input=move |ev| {
set_username(event_target_value(&ev));
}
required/>
</label>
<label>
<span>"Password"</span>
<input type="password" name="password" required/>
<input
type="password"
name="password"
prop:value=password
on:input=move |ev| {
set_password(event_target_value(&ev));
}
required/>
</label>
<button type="submit" class="primary">"Login"</button>
</ActionForm>
Expand Down
2 changes: 2 additions & 0 deletions src/views/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod logout;
mod profile;
mod register;
mod settings;
mod verify;

pub use self::code::*;
pub use self::home::*;
Expand All @@ -15,3 +16,4 @@ pub use self::logout::*;
pub use self::profile::*;
pub use self::register::*;
pub use self::settings::*;
pub use self::verify::*;
Loading

0 comments on commit 3e9b180

Please sign in to comment.