Skip to content
Snippets Groups Projects
account.rs 9.38 KiB
Newer Older
insert's avatar
insert committed
use super::Response;
insert's avatar
insert committed
use crate::database;
insert's avatar
insert committed
use crate::email;
use crate::util::gen_token;
insert's avatar
no  
insert committed

insert's avatar
insert committed
use bcrypt::{hash, verify};
use bson::{doc, from_bson, Bson::UtcDatetime};
use chrono::prelude::*;
insert's avatar
insert committed
use database::user::User;
use rocket_contrib::json::Json;
insert's avatar
insert committed
use serde::{Deserialize, Serialize};
insert's avatar
insert committed
use validator::validate_email;
insert's avatar
insert committed

insert's avatar
no  
insert committed
#[derive(Serialize, Deserialize)]
pub struct Create {
insert's avatar
insert committed
    username: String,
    password: String,
    email: String,
insert's avatar
insert committed
}

/// create a new Revolt account
/// (1) validate input
/// 	[username] 2 to 32 characters
/// 	[password] 8 to 72 characters
/// 	[email]    validate against RFC
/// (2) check email existence
/// (3) add user and send email verification
insert's avatar
no  
insert committed
#[post("/create", data = "<info>")]
insert's avatar
insert committed
pub fn create(info: Json<Create>) -> Response {
insert's avatar
insert committed
    let col = database::get_collection("users");

    if info.username.len() < 2 || info.username.len() > 32 {
insert's avatar
insert committed
        return Response::NotAcceptable(
            json!({ "error": "Username needs to be at least 2 chars and less than 32 chars." }),
        );
insert's avatar
insert committed
    }

    if info.password.len() < 8 || info.password.len() > 72 {
insert's avatar
insert committed
        return Response::NotAcceptable(
            json!({ "error": "Password needs to be at least 8 chars and at most 72." }),
        );
insert's avatar
insert committed
    }

    if !validate_email(info.email.clone()) {
insert's avatar
insert committed
        return Response::UnprocessableEntity(json!({ "error": "Invalid email." }));
insert's avatar
insert committed
    }

    if let Some(_) = col
        .find_one(doc! { "email": info.email.clone() }, None)
        .expect("Failed user lookup")
    {
insert's avatar
insert committed
        return Response::Conflict(json!({ "error": "Email already in use!" }));
insert's avatar
insert committed
    }

    if let Ok(hashed) = hash(info.password.clone(), 10) {
        let access_token = gen_token(92);
        let code = gen_token(48);

        match col.insert_one(
            doc! {
                "_id": Ulid::new().to_string(),
                "email": info.email.clone(),
                "username": info.username.clone(),
                "password": hashed,
                "access_token": access_token,
                "email_verification": {
                    "verified": false,
                    "target": info.email.clone(),
                    "expiry": UtcDatetime(Utc::now() + chrono::Duration::days(1)),
                    "rate_limit": UtcDatetime(Utc::now() + chrono::Duration::minutes(1)),
                    "code": code.clone(),
                }
            },
            None,
        ) {
            Ok(_) => {
                let sent = email::send_verification_email(info.email.clone(), code);

insert's avatar
insert committed
                Response::Success(json!({
insert's avatar
insert committed
                    "email_sent": sent,
insert's avatar
insert committed
                }))
            }
            Err(_) => {
                Response::InternalServerError(json!({ "error": "Failed to create account." }))
insert's avatar
insert committed
            }
        }
    } else {
insert's avatar
insert committed
        Response::InternalServerError(json!({ "error": "Failed to hash." }))
insert's avatar
insert committed
    }
insert's avatar
insert committed
}
insert's avatar
insert committed

/// verify an email for a Revolt account
/// (1) check if code is valid
/// (2) check if it expired yet
/// (3) set account as verified
#[get("/verify/<code>")]
insert's avatar
insert committed
pub fn verify_email(code: String) -> Response {
insert's avatar
insert committed
    let col = database::get_collection("users");

    if let Some(u) = col
        .find_one(doc! { "email_verification.code": code.clone() }, None)
        .expect("Failed user lookup")
    {
        let user: User = from_bson(bson::Bson::Document(u)).expect("Failed to unwrap user.");
        let ev = user.email_verification;

        if Utc::now() > *ev.expiry.unwrap() {
insert's avatar
insert committed
            Response::Gone(json!({
insert's avatar
insert committed
                "success": false,
                "error": "Token has expired!",
insert's avatar
insert committed
            }))
insert's avatar
insert committed
        } else {
            let target = ev.target.unwrap();
            col.update_one(
                doc! { "_id": user.id },
                doc! {
                    "$unset": {
                        "email_verification.code": "",
                        "email_verification.expiry": "",
                        "email_verification.target": "",
                        "email_verification.rate_limit": "",
                    },
                    "$set": {
                        "email_verification.verified": true,
                        "email": target.clone(),
                    },
                },
                None,
            )
            .expect("Failed to update user!");

            email::send_welcome_email(target.to_string(), user.username);

insert's avatar
insert committed
            Response::Redirect(
                super::Redirect::to("https://example.com"), // ! FIXME; redirect to landing page
            )
insert's avatar
insert committed
        }
    } else {
insert's avatar
insert committed
        Response::BadRequest(json!({ "error": "Invalid code." }))
insert's avatar
insert committed
    }
insert's avatar
insert committed
}
insert's avatar
insert committed

#[derive(Serialize, Deserialize)]
pub struct Resend {
insert's avatar
insert committed
    email: String,
insert's avatar
insert committed
}

/// resend a verification email
/// (1) check if verification is pending for x email
/// (2) check for rate limit
/// (3) resend the email
#[post("/resend", data = "<info>")]
insert's avatar
insert committed
pub fn resend_email(info: Json<Resend>) -> Response {
insert's avatar
insert committed
    let col = database::get_collection("users");

    if let Some(u) = col
        .find_one(
            doc! { "email_verification.target": info.email.clone() },
            None,
        )
        .expect("Failed user lookup.")
insert's avatar
insert committed
    {
        let user: User = from_bson(bson::Bson::Document(u)).expect("Failed to unwrap user.");
        let ev = user.email_verification;

        let expiry = ev.expiry.unwrap();
        let rate_limit = ev.rate_limit.unwrap();

        if Utc::now() < *rate_limit {
insert's avatar
insert committed
            Response::TooManyRequests(
                json!({ "error": "You are being rate limited, please try again in a while." }),
            )
insert's avatar
insert committed
        } else {
            let mut new_expiry = UtcDatetime(Utc::now() + chrono::Duration::days(1));
            if info.email.clone() != user.email {
                if Utc::now() > *expiry {
insert's avatar
insert committed
                    return Response::Gone(
                        json!({ "error": "To help protect your account, please login and change your email again. The original request was made over one day ago." }),
                    );
insert's avatar
insert committed
                }

                new_expiry = UtcDatetime(*expiry);
            }

            let code = gen_token(48);
            col.update_one(
insert's avatar
insert committed
					doc! { "_id": user.id },
insert's avatar
insert committed
					doc! {
						"$set": {
							"email_verification.code": code.clone(),
							"email_verification.expiry": new_expiry,
insert's avatar
insert committed
							"email_verification.rate_limit": UtcDatetime(Utc::now() + chrono::Duration::minutes(1)),
						},
					},
					None,
				).expect("Failed to update user!");

insert's avatar
insert committed
            match email::send_verification_email(info.email.to_string(), code) {
                true => Response::Result(super::Status::Ok),
insert's avatar
insert committed
                false => Response::InternalServerError(
insert's avatar
insert committed
                    json!({ "error": "Failed to send email! Likely an issue with the backend API." }),
insert's avatar
insert committed
                ),
insert's avatar
insert committed
            }
        }
    } else {
insert's avatar
insert committed
        Response::NotFound(
insert's avatar
insert committed
            json!({ "error": "Email not found or pending verification!" }),
insert's avatar
insert committed
        )
insert's avatar
insert committed
    }
insert's avatar
insert committed
}
insert's avatar
insert committed

#[derive(Serialize, Deserialize)]
pub struct Login {
insert's avatar
insert committed
    email: String,
    password: String,
insert's avatar
insert committed
}

#[options("/login")]
pub fn login_preflight() -> Response {
    Response::Result(super::Status::Ok)
}

insert's avatar
insert committed
/// login to a Revolt account
/// (1) find user by email
/// (2) verify password
/// (3) return access token
#[post("/login", data = "<info>")]
insert's avatar
insert committed
pub fn login(info: Json<Login>) -> Response {
insert's avatar
insert committed
    let col = database::get_collection("users");

    if let Some(u) = col
        .find_one(doc! { "email": info.email.clone() }, None)
        .expect("Failed user lookup")
    {
        let user: User = from_bson(bson::Bson::Document(u)).expect("Failed to unwrap user.");

        match verify(info.password.clone(), &user.password)
            .expect("Failed to check hash of password.")
        {
            true => {
                let token = match user.access_token {
                    Some(t) => t.to_string(),
                    None => {
                        let token = gen_token(92);
                        if col
                            .update_one(
                                doc! { "_id": &user.id },
                                doc! { "$set": { "access_token": token.clone() } },
                                None,
                            )
                            .is_err()
                        {
                            return Response::InternalServerError(
                                json!({ "error": "Failed database operation." }),
                            );
insert's avatar
insert committed
                        }
insert's avatar
insert committed
                        token
                    }
                };

insert's avatar
insert committed
                Response::Success(json!({ "access_token": token, "id": user.id }))
insert's avatar
insert committed
            }
insert's avatar
insert committed
            false => Response::Unauthorized(json!({ "error": "Invalid password." })),
insert's avatar
insert committed
        }
    } else {
insert's avatar
insert committed
        Response::NotFound(json!({ "error": "Email is not registered." }))
insert's avatar
insert committed
    }
insert's avatar
insert committed
}
insert's avatar
insert committed

#[derive(Serialize, Deserialize)]
pub struct Token {
insert's avatar
insert committed
    token: String,
insert's avatar
insert committed
}

/// login to a Revolt account via token
#[post("/token", data = "<info>")]
insert's avatar
insert committed
pub fn token(info: Json<Token>) -> Response {
insert's avatar
insert committed
    let col = database::get_collection("users");

    if let Ok(result) = col.find_one(doc! { "access_token": info.token.clone() }, None) {
insert's avatar
insert committed
        if let Some(user) = result {
            Response::Success(json!({
                "id": user.get_str("_id").unwrap(),
            }))
        } else {
            Response::Unauthorized(json!({
                "error": "Invalid token!",
            }))
        }
insert's avatar
insert committed
    } else {
insert's avatar
insert committed
        Response::InternalServerError(json!({
            "error": "Failed database query.",
insert's avatar
insert committed
    }
insert's avatar
insert committed
}