From dd3bbeabd82eefb74251b4e8b68352964be3b0f5 Mon Sep 17 00:00:00 2001 From: shockrah Date: Tue, 2 Jun 2020 03:44:33 -0700 Subject: [PATCH] InviteRow structure has some methods listed below: + new() -> create a new ready to insert invite + from_tuple -> helps us translate db responses into a struct + as_json_str -> String which we can use as a json payload(jank but whatever it works with proper headers) FUnctions: get_invite_by_code: as the name implies grabs an invite assuming its in our db otherwise None record_invite_usage: blindly modifies the row in the schema, assumes that if we get to this point we're ok to ignore erroneous possibilities join_invite_code: main dispatcher for joining using the /invite/join route create_invite: meant to the be the route handler for creating invites later on Needed: test module since we've basically reworked the whole auth/invite system in one day --- server/src/invites.rs | 197 ++++++++++++++++++++++-------------------- 1 file changed, 104 insertions(+), 93 deletions(-) diff --git a/server/src/invites.rs b/server/src/invites.rs index ad53d8c..633de47 100644 --- a/server/src/invites.rs +++ b/server/src/invites.rs @@ -1,107 +1,118 @@ -// Module handles creating invites for potentially new users -use diesel::{self, prelude::*}; -use rocket_contrib::json::Json; -use chrono::{Duration, Utc}; +use std::collections::HashMap; + +use mysql_async; +use mysql_async::Conn; +use mysql_async::error::Error; +use mysql_async::prelude::{params, Queryable}; + +use hyper::{Response, Body, StatusCode}; + +use chrono::Utc; use rand::random; +struct InviteRow { + id: u64, + expires: u64, + uses: i32, +} -use crate::auth::{join, AuthResult, AuthErr}; -use crate::DBConn; -use crate::models::{User, Invite}; -use crate::schema; - - -/* -TODO: both the generation and usage endpoints for invites need the following - * meaningful responses - * authentication -*/ - -#[get("/generate")] -pub fn generate_invite(conn: DBConn) -> Json { - let dt = Utc::now() + Duration::minutes(30); - // TODO:[maybe] ensure no collisions by doing a quick database check here - let mut new_invite = Invite { - id: random::(), // hopefully there won't ever be collision with this size of pool - uses: 1, // default/hardcorded for now - expires: dt.timestamp() as u64 - }; - // Next we cache this invite - let result = diesel::insert_into(schema::invites::table) - .values(&new_invite) - .execute(&conn.0); - - // Finally we attempt to return _something_ - match result { - Ok(_val) => { - Json(new_invite) - } - Err(_e) => { - new_invite.id = 0; - new_invite.expires = 0; - new_invite.uses = 0; - Json(new_invite) +impl InviteRow { + pub fn new() -> InviteRow { + let dt = Utc::now() + chrono::Duration::minutes(30); + // TODO:[maybe] ensure no collisions by doing a quick database check here + let mut invite = InviteRow { + id: random::(), // hopefully there won't ever be collision with this size of pool + uses: 1, // default/hardcorded for now + expires: dt.timestamp() as u64 + }; + invite + } + pub fn from_tuple(tup: (u64, u64, i32)) -> InviteRow { + InviteRow { + id: tup.0, + expires: tup.1, + uses: tup.2, } } + + pub fn as_json_str(&self) -> String { + let id = format!("\"id\":{}", self.id); + let expires = format!("\"expires\":{}", self.expires); + let uses = format!("\"uses\":{}", self.uses); + + let mut data = String::from("{"); + data.push_str(&format!("{},", id)); + data.push_str(&format!("{},", expires)); + data.push_str(&format!("{}}}", uses)); + data + } } -// GET doesn't really like having data in its body for whatever reason -// Problem: invite/joining system is gonna have to get a redesign -#[get("/join//")] -pub fn use_invite(hash: u64, name: String, conn: DBConn) -> AuthResult, AuthErr>{ - join(conn, hash, name) +async fn get_invite_by_code(conn: &Conn, value: Option<&str>) -> Option { + if let Some(val) = value { + let db_row_result: Result<(Conn, Option<(u64, u64, i32)>), Error> = conn + .first_exec(r"SELECT * FROM", mysql_async::params!{"code"=>}) + .await; + match db_row_result { + Ok(data) => { + if let Some(tup) = data.1 {Some(InviteRow::from_tuple(tup))} + else {None} + } + Err(_) => None, + } + } + else { + None + } } -#[cfg(test)] -mod invite_tests { - use super::*; - use rocket; - use rocket::local::Client; - use rocket::http::Status; - use serde_json; +async fn record_invite_usage(conn: &Conn, data: &InviteRow) { + /* + * By this this is called we really don't care about what happens as we've + * already been querying the db and the likely hood of this seriously failing + * is low enough to write a wall of text and not a wall of error handling code + */ + let _db_result = conn + .prep_exec(r"UPDATE invites SET uses = :uses WHERE id = :id", mysql_async::params!{ + "uses" => data.uses - 1, + "id" => data.id + }).await; +} - #[test] - fn request_invite() { - let rocket = rocket::ignite() - .mount("/invite", routes![generate_invite]) - .attach(DBConn::fairing()); - - let client = Client::new(rocket).expect("Invalid rocket instance"); - let mut response = client.get("/invite/generate").dispatch(); - - assert_eq!(response.status(), Status::Ok); - match response.body_string() { - Some(val) => { - let invite: Result = serde_json::from_str(&val); - match invite { - Ok(val) => { - println!("{:#?}", val) - } - Err(e) => { - panic!("{}", e) - } +pub async fn join_invite_code(conn: &Conn, response: &mut Response, params: &HashMap<&str, &str>) { + // First check that the code is there + match params.get("code") { + Some(p) => { + if let Some(row) = get_invite_by_code(conn, Some(*p)).await { + // since we have a row make sure the invite is valid + let now = Utc::now().timestamp() as u64; + if row.uses > 0 && row.expires > now { + record_invite_usage(conn, &row).await; + // TODO: assign some actual data to the body + *response.status_mut() = StatusCode::OK; } } - None => { - println!("bro wtf"); - } + }, + None => { + *response.status_mut() = StatusCode::BAD_REQUEST; } } - - #[test] - fn use_invite() { - let app = rocket::ignite() - .mount("/invite", routes![generate_invite, use_invite]) - .attach(DBConn::fairing()); - - // We assume in this test that invite generation is fine - let client = Client::new(app).expect("Invalid rocket instance"); - let mut r = client.get("/invite/generate").dispatch(); - let invite: Invite = serde_json::from_str(&r.body_string().unwrap()).unwrap(); - - // TODO: for some reason we can't use a regular struct so figure that out at some point - let mut response = client.get(format!("/invite/{}", invite.id)).dispatch(); - let body: String = response.body_string().unwrap(); - println!("{}", body); - } - } + +pub async fn create_invite(conn: &Conn, response: &mut Response) { + let invite = InviteRow::new(); + let db_result = conn + .prep_exec(r"INSERT INTO invites (id, expires, uses) VALUES (:id, :expires, :uses", + mysql_async::params!{ + "id" => invite.id, + "expires" => invite.expires, + "uses" => invite.uses + }).await; + + match db_result { + Ok(d) => { + *response.body_mut() = Body::from(invite::as_json_str()); + *response.status_mut() = StatusCode::OK; + } + Err(e) => {} + } +} \ No newline at end of file