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
This commit is contained in:
parent
43a122b855
commit
dd3bbeabd8
@ -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<Invite> {
|
||||
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::<u64>(), // 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::<u64>(), // 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/<hash>/<name>")]
|
||||
pub fn use_invite(hash: u64, name: String, conn: DBConn) -> AuthResult<Json<User>, AuthErr>{
|
||||
join(conn, hash, name)
|
||||
async fn get_invite_by_code(conn: &Conn, value: Option<&str>) -> Option<InviteRow> {
|
||||
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<Invite, serde_json::Error> = 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<Body>, 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<Body>) {
|
||||
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) => {}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user