From 0021ada63946aaa3ef1383f19e0694e81726e36a Mon Sep 17 00:00:00 2001
From: Paul Makles <paulmakles@gmail.com>
Date: Sat, 25 Jan 2020 11:33:35 +0000
Subject: [PATCH] Clean up errors, add /users route.

---
 src/auth.rs           | 16 +++++++++-------
 src/routes/account.rs | 44 ++++++++++++++++++++++++-------------------
 src/routes/mod.rs     |  4 +++-
 src/routes/user.rs    | 18 ++++++++++++++++++
 4 files changed, 55 insertions(+), 27 deletions(-)
 create mode 100644 src/routes/user.rs

diff --git a/src/auth.rs b/src/auth.rs
index 8fbdc91..349311b 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -2,11 +2,13 @@ use rocket::Outcome;
 use rocket::http::Status;
 use rocket::request::{self, Request, FromRequest};
 
-use bson::{ bson, doc, ordered::OrderedDocument, oid::ObjectId };
+use bson::{ bson, doc, ordered::OrderedDocument };
+use ulid::Ulid;
+
 use crate::database;
 
 pub struct User(
-	pub ObjectId,
+	pub Ulid,
 	pub String,
 	pub OrderedDocument,
 );
@@ -24,20 +26,20 @@ impl<'a, 'r> FromRequest<'a, 'r> for User {
     fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
         let keys: Vec<_> = request.headers().get("x-auth-token").collect();
         match keys.len() {
-			0 => Outcome::Failure((Status::BadRequest, AuthError::Missing)),
+			0 => Outcome::Failure((Status::Forbidden, AuthError::Missing)),
 			1 => {
 				let key = keys[0];
 				let col = database::get_db().collection("users");
-				let result = col.find_one(Some( doc! { "auth_token": key } ), None).unwrap();
+				let result = col.find_one(doc! { "access_token": key }, None).unwrap();
 
 				if let Some(user) = result {
 					Outcome::Success(User(
-						user.get_object_id("_id").unwrap().clone(),
-						user.get_str("username").unwrap().to_owned(),
+						Ulid::from_string(user.get_str("_id").unwrap()).unwrap(),
+						user.get_str("username").unwrap().to_string(),
 						user
 					))
 				} else {
-					Outcome::Failure((Status::BadRequest, AuthError::Invalid))
+					Outcome::Failure((Status::Forbidden, AuthError::Invalid))
 				}
 			},
             _ => Outcome::Failure((Status::BadRequest, AuthError::BadCount)),
diff --git a/src/routes/account.rs b/src/routes/account.rs
index 689a2f1..8f9147a 100644
--- a/src/routes/account.rs
+++ b/src/routes/account.rs
@@ -18,13 +18,6 @@ fn gen_token(l: usize) -> String {
 		.collect::<String>()
 }
 
-#[get("/")]
-pub fn root(user: User) -> String {
-	let User ( id, username, _doc ) = user;
-
-	format!("hello, {}! [id: {}]", username, id)
-}
-
 #[derive(Serialize, Deserialize)]
 pub struct Create {
 	username: String,
@@ -72,7 +65,7 @@ pub fn create(info: Json<Create>) -> JsonValue {
 	}
 
 	if let Ok(hashed) = hash(info.password.clone(), 10) {
-		let access_token = gen_token(64);
+		let access_token = gen_token(92);
 		let code = gen_token(48);
 
 		match col.insert_one(doc! {
@@ -120,8 +113,8 @@ pub fn verify_email(code: String) -> JsonValue {
 
 	if let Some(u) =
 		col.find_one(doc! { "email_verification.code": code.clone() }, None).expect("Failed user lookup") {
-			let ev = u.get_document("email_verification").expect("Missing email_verification on user object!");
-			let expiry = ev.get_utc_datetime("expiry").expect("Missing expiry date on email_verification!");
+			let ev = u.get_document("email_verification").expect("DOC[email_verification]");
+			let expiry = ev.get_utc_datetime("expiry").expect("DOC[expiry]");
 
 			if Utc::now() > *expiry {
 				json!({
@@ -129,7 +122,7 @@ pub fn verify_email(code: String) -> JsonValue {
 					"error": "Token has expired!",
 				})
 			} else {
-				let target = ev.get_str("target").expect("Missing target email on email_verification!");
+				let target = ev.get_str("target").expect("DOC[target]");
 				col.update_one(
 					doc! { "_id": u.get_str("_id").expect("Failed to retrieve user id.") },
 					doc! {
@@ -179,22 +172,35 @@ pub fn resend_email(info: Json<Resend>) -> JsonValue {
 
 	if let Some(u) =
 		col.find_one(doc! { "email_verification.target": info.email.clone() }, None).expect("Failed user lookup") {
-			let ev = u.get_document("email_verification").expect("Missing email_verification on user object!");
-			let rate_limit = ev.get_utc_datetime("rate_limit").expect("Missing rate_limit on email_verification!");
+			let ev = u.get_document("email_verification").expect("DOC[email_verification]");
+			let expiry = ev.get_utc_datetime("expiry").expect("DOC[expiry]");
+			let rate_limit = ev.get_utc_datetime("rate_limit").expect("DOC[rate_limit]");
 
 			if Utc::now() < *rate_limit {
 				json!({
 					"success": false,
-					"error": "Hit rate limit! Please try again in a minute or so."
+					"error": "Hit rate limit! Please try again in a minute or so.",
 				})
 			} else {
+				let mut new_expiry = UtcDatetime(Utc::now() + chrono::Duration::days(1));
+				if info.email.clone() != u.get_str("email").expect("DOC[email]") {
+					if Utc::now() > *expiry {
+						return json!({
+							"success": "false",
+							"error": "For security reasons, please login and change your email again.",
+						})
+					}
+
+					new_expiry = UtcDatetime(*expiry);
+				}
+
 				let code = gen_token(48);
 				col.update_one(
 					doc! { "_id": u.get_str("_id").expect("Failed to retrieve user id.") },
 					doc! {
 						"$set": {
 							"email_verification.code": code.clone(),
-							"email_verification.expiry": UtcDatetime(Utc::now() + chrono::Duration::days(1)),
+							"email_verification.expiry": new_expiry,
 							"email_verification.rate_limit": UtcDatetime(Utc::now() + chrono::Duration::minutes(1)),
 						},
 					},
@@ -210,7 +216,7 @@ pub fn resend_email(info: Json<Resend>) -> JsonValue {
 					}),
 					false => json!({
 						"success": false,
-						"error": "Failed to send email! Likely an issue with the backend API."
+						"error": "Failed to send email! Likely an issue with the backend API.",
 					})
 				}
 			}
@@ -238,16 +244,16 @@ pub fn login(info: Json<Login>) -> JsonValue {
 
 	if let Some(u) =
 		col.find_one(doc! { "email": info.email.clone() }, None).expect("Failed user lookup") {
-			match verify(info.password.clone(), u.get_str("password").expect("Missing password in user object!"))
+			match verify(info.password.clone(), u.get_str("password").expect("DOC[password]"))
 				.expect("Failed to check hash of password.") {
 					true => {
 						let token =
 							match u.get_str("access_token") {
 								Ok(t) => t.to_string(),
 								Err(_) => {
-									let token = gen_token(64);
+									let token = gen_token(92);
 									col.update_one(
-										doc! { "_id": u.get_str("_id").expect("Missing id in user object!") },
+										doc! { "_id": u.get_str("_id").expect("DOC[id]") },
 										doc! { "$set": { "access_token": token.clone() } },
 										None
 									).expect("Failed to update user object");
diff --git a/src/routes/mod.rs b/src/routes/mod.rs
index 14f1a24..8b2aeb5 100644
--- a/src/routes/mod.rs
+++ b/src/routes/mod.rs
@@ -1,8 +1,10 @@
 use rocket::Rocket;
 
 mod account;
+mod user;
 
 pub fn mount(rocket: Rocket) -> Rocket {
 	rocket
-		.mount("/api/account", routes![ account::root, account::create, account::verify_email, account::resend_email, account::login ])
+		.mount("/api/account", routes![ account::create, account::verify_email, account::resend_email, account::login ])
+		.mount("/api/users", routes![ user::me ])
 }
diff --git a/src/routes/user.rs b/src/routes/user.rs
new file mode 100644
index 0000000..7515ea2
--- /dev/null
+++ b/src/routes/user.rs
@@ -0,0 +1,18 @@
+use crate::auth::User;
+
+use rocket_contrib::json::{ JsonValue };
+use bson::{ doc };
+
+#[get("/@me")]
+pub fn me(user: User) -> JsonValue {
+	let User ( id, username, doc ) = user;
+
+	json!({
+		"id": id.to_string(),
+		"username": username,
+		"email": doc.get_str("email").expect("Missing email in user object!"),
+		"verified": doc.get_document("email_verification").expect("DOC[email_verification]")
+						.get_bool("verified").expect("DOC[verified]"),
+		"created_timestamp": id.datetime().timestamp(),
+	})
+}
-- 
GitLab