From ca0b8411d58ce7250ded461b19f3356dd96a51a4 Mon Sep 17 00:00:00 2001
From: Paul Makles <paulmakles@gmail.com>
Date: Tue, 7 Apr 2020 12:08:07 +0100
Subject: [PATCH] Use proper error handling.

---
 src/database/channel.rs |   2 +-
 src/database/guild.rs   |  28 +++++----
 src/routes/account.rs   | 132 ++++++++++++++++------------------------
 src/routes/guild.rs     |   7 ++-
 src/routes/mod.rs       |  33 ++++++++++
 5 files changed, 107 insertions(+), 95 deletions(-)

diff --git a/src/database/channel.rs b/src/database/channel.rs
index 1aba7e2..2b10748 100644
--- a/src/database/channel.rs
+++ b/src/database/channel.rs
@@ -6,7 +6,7 @@ pub struct Channel {
     pub id: String,
     #[serde(rename = "type")]
     pub channel_type: u8,
-   
+
     pub last_message: Option<String>,
 
     // for Direct Messages
diff --git a/src/database/guild.rs b/src/database/guild.rs
index 51a74aa..5414e44 100644
--- a/src/database/guild.rs
+++ b/src/database/guild.rs
@@ -1,5 +1,5 @@
-use serde::{Deserialize, Serialize};
 use bson::{bson, doc};
+use serde::{Deserialize, Serialize};
 
 use super::get_collection;
 use mongodb::options::FindOneOptions;
@@ -15,7 +15,11 @@ bitfield! {
     pub get_send_messages, set_send_messages: 2;
 }
 
-pub fn find_member_permissions<C: Into<Option<String>>>(id: String, guild: String, channel: C) -> u8 {
+pub fn find_member_permissions<C: Into<Option<String>>>(
+    id: String,
+    guild: String,
+    channel: C,
+) -> u8 {
     let col = get_collection("guilds");
 
     match col.find_one(
@@ -28,27 +32,25 @@ pub fn find_member_permissions<C: Into<Option<String>>>(id: String, guild: Strin
             }
         },
         FindOneOptions::builder()
-            .projection(
-                doc! {
-                    "members.$": 1,
-                    "owner": 1,
-                    "default_permissions": 1,
-                }
-            )
-            .build()
+            .projection(doc! {
+                "members.$": 1,
+                "owner": 1,
+                "default_permissions": 1,
+            })
+            .build(),
     ) {
         Ok(result) => {
             if let Some(doc) = result {
                 if doc.get_str("owner").unwrap() == id {
                     return u8::MAX;
                 }
-                
+
                 doc.get_i32("default_permissions").unwrap() as u8
             } else {
                 0
             }
-        },
-        Err(_) => 0
+        }
+        Err(_) => 0,
     }
 }
 
diff --git a/src/routes/account.rs b/src/routes/account.rs
index 487ce52..cea602f 100644
--- a/src/routes/account.rs
+++ b/src/routes/account.rs
@@ -1,3 +1,4 @@
+use super::Response;
 use crate::database;
 use crate::email;
 
@@ -33,38 +34,30 @@ pub struct Create {
 /// (2) check email existence
 /// (3) add user and send email verification
 #[post("/create", data = "<info>")]
-pub fn create(info: Json<Create>) -> JsonValue {
+pub fn create(info: Json<Create>) -> Response {
     let col = database::get_collection("users");
 
     if info.username.len() < 2 || info.username.len() > 32 {
-        return json!({
-            "success": false,
-            "error": "Username requirements not met! Must be between 2 and 32 characters.",
-        });
+        return Response::NotAcceptable(
+            json!({ "error": "Username needs to be at least 2 chars and less than 32 chars." }),
+        );
     }
 
     if info.password.len() < 8 || info.password.len() > 72 {
-        return json!({
-            "success": false,
-            "error": "Password requirements not met! Must be between 8 and 72 characters.",
-        });
+        return Response::NotAcceptable(
+            json!({ "error": "Password needs to be at least 8 chars and at most 72." }),
+        );
     }
 
     if !validate_email(info.email.clone()) {
-        return json!({
-            "success": false,
-            "error": "Invalid email provided!",
-        });
+        return Response::UnprocessableEntity(json!({ "error": "Invalid email." }));
     }
 
     if let Some(_) = col
         .find_one(doc! { "email": info.email.clone() }, None)
         .expect("Failed user lookup")
     {
-        return json!({
-            "success": false,
-            "error": "Email already in use!",
-        });
+        return Response::Conflict(json!({ "error": "Email already in use!" }));
     }
 
     if let Ok(hashed) = hash(info.password.clone(), 10) {
@@ -91,21 +84,17 @@ pub fn create(info: Json<Create>) -> JsonValue {
             Ok(_) => {
                 let sent = email::send_verification_email(info.email.clone(), code);
 
-                json!({
+                Response::Success(json!({
                     "success": true,
                     "email_sent": sent,
-                })
+                }))
+            }
+            Err(_) => {
+                Response::InternalServerError(json!({ "error": "Failed to create account." }))
             }
-            Err(_) => json!({
-                "success": false,
-                "error": "Failed to create account!",
-            }),
         }
     } else {
-        json!({
-            "success": false,
-            "error": "Failed to hash password!",
-        })
+        Response::InternalServerError(json!({ "error": "Failed to hash." }))
     }
 }
 
@@ -114,7 +103,7 @@ pub fn create(info: Json<Create>) -> JsonValue {
 /// (2) check if it expired yet
 /// (3) set account as verified
 #[get("/verify/<code>")]
-pub fn verify_email(code: String) -> JsonValue {
+pub fn verify_email(code: String) -> Response {
     let col = database::get_collection("users");
 
     if let Some(u) = col
@@ -125,10 +114,10 @@ pub fn verify_email(code: String) -> JsonValue {
         let ev = user.email_verification;
 
         if Utc::now() > *ev.expiry.unwrap() {
-            json!({
+            Response::Gone(json!({
                 "success": false,
                 "error": "Token has expired!",
-            })
+            }))
         } else {
             let target = ev.target.unwrap();
             col.update_one(
@@ -151,15 +140,12 @@ pub fn verify_email(code: String) -> JsonValue {
 
             email::send_welcome_email(target.to_string(), user.username);
 
-            json!({
-                "success": true
-            })
+            Response::Redirect(
+                super::Redirect::to("https://example.com"), // ! FIXME; redirect to landing page
+            )
         }
     } else {
-        json!({
-            "success": false,
-            "error": "Invalid code!",
-        })
+        Response::BadRequest(json!({ "error": "Invalid code." }))
     }
 }
 
@@ -173,7 +159,7 @@ pub struct Resend {
 /// (2) check for rate limit
 /// (3) resend the email
 #[post("/resend", data = "<info>")]
-pub fn resend_email(info: Json<Resend>) -> JsonValue {
+pub fn resend_email(info: Json<Resend>) -> Response {
     let col = database::get_collection("users");
 
     if let Some(u) = col
@@ -190,18 +176,16 @@ pub fn resend_email(info: Json<Resend>) -> JsonValue {
         let rate_limit = ev.rate_limit.unwrap();
 
         if Utc::now() < *rate_limit {
-            json!({
-                "success": false,
-                "error": "Hit rate limit! Please try again in a minute or so.",
-            })
+            Response::TooManyRequests(
+                json!({ "error": "You are being rate limited, please try again in a while." }),
+            )
         } else {
             let mut new_expiry = UtcDatetime(Utc::now() + chrono::Duration::days(1));
             if info.email.clone() != user.email {
                 if Utc::now() > *expiry {
-                    return json!({
-                        "success": "false",
-                        "error": "For security reasons, please login and change your email again.",
-                    });
+                    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." }),
+                    );
                 }
 
                 new_expiry = UtcDatetime(*expiry);
@@ -221,20 +205,16 @@ pub fn resend_email(info: Json<Resend>) -> JsonValue {
 				).expect("Failed to update user!");
 
             match email::send_verification_email(info.email.to_string(), code) {
-                true => json!({
-                    "success": true,
-                }),
-                false => json!({
-                    "success": false,
-                    "error": "Failed to send email! Likely an issue with the backend API.",
-                }),
+                true => Response::Ok(None),
+                false => Response::InternalServerError(
+                    json!({ "success": false, "error": "Failed to send email! Likely an issue with the backend API." }),
+                ),
             }
         }
     } else {
-        json!({
-            "success": false,
-            "error": "Email not pending verification!",
-        })
+        Response::NotFound(
+            json!({ "success": false, "error": "Email not found or pending verification!" }),
+        )
     }
 }
 
@@ -249,7 +229,7 @@ pub struct Login {
 /// (2) verify password
 /// (3) return access token
 #[post("/login", data = "<info>")]
-pub fn login(info: Json<Login>) -> JsonValue {
+pub fn login(info: Json<Login>) -> Response {
     let col = database::get_collection("users");
 
     if let Some(u) = col
@@ -276,22 +256,12 @@ pub fn login(info: Json<Login>) -> JsonValue {
                     }
                 };
 
-                json!({
-                    "success": true,
-                    "access_token": token,
-                    "id": user.id
-                })
+                Response::Success(json!({ "access_token": token, "id": user.id }))
             }
-            false => json!({
-                "success": false,
-                "error": "Invalid password."
-            }),
+            false => Response::Unauthorized(json!({ "error": "Invalid password." })),
         }
     } else {
-        json!({
-            "success": false,
-            "error": "Email is not registered.",
-        })
+        Response::NotFound(json!({ "error": "Email is not registered." }))
     }
 }
 
@@ -302,21 +272,23 @@ pub struct Token {
 
 /// login to a Revolt account via token
 #[post("/token", data = "<info>")]
-pub fn token(info: Json<Token>) -> JsonValue {
+pub fn token(info: Json<Token>) -> Response {
     let col = database::get_collection("users");
 
     if let Some(u) = col
         .find_one(doc! { "access_token": info.token.clone() }, None)
         .expect("Failed user lookup")
     {
-        json!({
-            "success": true,
-            "id": u.get_str("_id").unwrap(),
-        })
+        Response::Success(
+            json!({
+                "id": u.get_str("_id").unwrap(),
+            })
+        )
     } else {
-        json!({
-            "success": false,
-            "error": "Invalid token!",
-        })
+        Response::Unauthorized(
+            json!({
+                "error": "Invalid token!",
+            })
+        )
     }
 }
diff --git a/src/routes/guild.rs b/src/routes/guild.rs
index 37538c2..c7c902e 100644
--- a/src/routes/guild.rs
+++ b/src/routes/guild.rs
@@ -1,4 +1,9 @@
-use crate::database::{self, channel::Channel, guild::{ Guild, find_member_permissions }, user::User};
+use crate::database::{
+    self,
+    channel::Channel,
+    guild::{find_member_permissions, Guild},
+    user::User,
+};
 
 use bson::{bson, doc, from_bson, Bson};
 use rocket_contrib::json::{Json, JsonValue};
diff --git a/src/routes/mod.rs b/src/routes/mod.rs
index 171155b..cb88d0a 100644
--- a/src/routes/mod.rs
+++ b/src/routes/mod.rs
@@ -1,4 +1,7 @@
+use rocket::http::Status;
+pub use rocket::response::Redirect;
 use rocket::Rocket;
+use rocket_contrib::json::JsonValue;
 
 pub mod account;
 pub mod channel;
@@ -6,6 +9,36 @@ pub mod guild;
 pub mod root;
 pub mod user;
 
+#[derive(Responder)]
+pub enum Response {
+    #[response()]
+    Ok(Option<JsonValue>),
+    #[response()]
+    Success(JsonValue),
+    #[response()]
+    Redirect(Redirect),
+    #[response(status = 400)]
+    BadRequest(JsonValue),
+    #[response(status = 401)]
+    Unauthorized(JsonValue),
+    #[response(status = 404)]
+    NotFound(JsonValue),
+    #[response(status = 406)]
+    NotAcceptable(JsonValue),
+    #[response(status = 409)]
+    Conflict(JsonValue),
+    #[response(status = 410)]
+    Gone(JsonValue),
+    #[response(status = 422)]
+    UnprocessableEntity(JsonValue),
+    #[response(status = 429)]
+    TooManyRequests(JsonValue),
+    #[response(status = 500)]
+    InternalServerError(JsonValue),
+    #[response()]
+    Error(Status),
+}
+
 pub fn mount(rocket: Rocket) -> Rocket {
     rocket
         .mount("/api", routes![root::root])
-- 
GitLab