freechat/server-api/src/invites.rs

219 lines
6.0 KiB
Rust

use serde_json::Value;
use serde::Serialize;
use mysql_async;
use mysql_async::{Conn, Pool};
use mysql_async::error::Error;
use mysql_async::prelude::{params, Queryable};
use hyper::{Response, Body, StatusCode};
use chrono::Utc;
use db::{UBigInt, BigInt};
use db::common::FromDB;
use db::member::Member;
#[derive(Serialize)]
struct Invite {
id: BigInt, // doubles as the timestamp for when it dies
uses: Option<BigInt>, // optional because some links are permanent
expires: bool,
}
/*
* Error handling:
* All errors raisable from this module come from mysql_async and thus
* are of the enum mysql_async::error::Error
*/
async fn valid_invite(pool: &Pool, id: BigInt) -> bool {
/*
* Fetches an invite from the database to check for validity
*/
let query: Option<db::invites::Invite> = match db::invites::Invite::get(pool, id as u64).await {
db::Response::Row(invite) => { Some(invite) },
_ => { None }
};
if let Some(invite) = query {
// if expires at all
if invite.expires {
let now = Utc::now().timestamp();
// old?
let mut valid_status = now > invite.id;
// used?
if invite.uses.is_some() && valid_status == false {
valid_status = invite.uses.unwrap() <= 0; // safe unwrap since we know its Some(_)
}
return valid_status
}
// no expiry date? no problem
return true
}
// prolly not a real id
return false
}
async fn use_invite(pool: &Pool, code: Option<BigInt>) -> Option<Member>{
/*
* Attempts to change the state of the current invite being provided
*/
use crate::auth;
use crate::perms::GENERAL_NEW;
let id = match code {
Some(id) => id,
None => 0
};
// some random comment
if valid_invite(pool, id).await {
let raw_secret = auth::generate_secret();
if let Ok(secret) = auth::encrypt_secret(&raw_secret) {
return match db::member::Member::add(pool, "Anonymous".into(), &secret, GENERAL_NEW).await {
Ok(response) => {
match response {
db::Response::Row(member) => Some(member),
_ => None,
}
},
// TODO: logggin or something idk
Err(_) => return None
}
}
// Returning None because we couldn't actually create a proper secret to store
else {
return None;
}
}
// The invite itself was not valid
else {
return None;
}
}
pub async fn join(pool: &Pool, response: &mut Response<Body>, params: Value) {
/*
* Main dispatcher for dealing with an attempted join via a given invide code
*/
let code = match params.get("invite-id") {
Some(val) => val.as_i64(),
None => None
};
match use_invite(&pool, code).await {
Some(new_account) => *response.body_mut() = Body::from(serde_json::to_string(&new_account).unwrap()),
None => {
}
}
}
async fn insert_new_invite(pool: &Pool, invite: &Invite) -> Result<(), Error>{
let conn = pool.get_conn().await?;
conn.prep_exec(
"INSERT INTO invites (id, uses, expires)
VALUES (:id, :uses, :expires)", params!{
"id" => invite.id,
"uses" => invite.uses,
"expires" => invite.expires
}).await?;
Ok(())
}
async fn process_expires_parameter(p: &Pool, exp: &Value, id: UBigInt) -> bool {
// TODO: fix this somewhat unsafe code
// NOTE: its unsafe because of these lazy as heck unwraps everywhere
use crate::perms::{CREATE_PERM_INVITES, CREATE_TMP_INVITES};
let conn = p.get_conn().await.unwrap();
let db_tup: (Conn, Option<UBigInt>) = conn.first_exec(
"SELECT permissions FROM members WHERE id = :id",
params!{"id" => id})
.await.unwrap();
// depending on what type of invite we requested we should make sure we have the
// right permissions to do so
let real_perms = db_tup.1.unwrap(); // safe via auth module
if let Some(exp) = exp.as_bool() {
// perma?
if exp {
return (real_perms & CREATE_PERM_INVITES) == CREATE_PERM_INVITES;
}
else {
return (real_perms & CREATE_TMP_INVITES) == CREATE_TMP_INVITES;
}
}
else {
return false;
}
}
pub async fn create(pool: &Pool, response: &mut Response<Body>, params: Value) {
/*
* Creates a new invite
*/
// no user can actually have an id of 0 this won't find anyone on the backend
let id = match params.get("id") {
Some(val) => val.as_u64().unwrap_or(0),
None => 0
};
let use_count = match params.get("uses") {
Some(val) => val.as_i64(),
None => None
};
let expires = match params.get("expires") {
Some(exp_val) => process_expires_parameter(pool, exp_val, id).await,
None => true
};
let invite = Invite {
id: (Utc::now() + chrono::Duration::minutes(30)).timestamp(),
uses: use_count,
expires: expires
};
match insert_new_invite(&pool, &invite).await {
Ok(_) => {},
Err(mysqle) => {
println!("\tINVITES::CREATE::ERROR: {}", mysqle);
*response.status_mut() = StatusCode::BAD_REQUEST;
}
}
}
#[cfg(test)]
mod invites_test {
/*
* INVITE CREATION
* Good - Bad - Malicious
*/
use crate::testing::{get_pool, hyper_resp};
use hyper::StatusCode;
use serde_json::Value;
#[tokio::test]
async fn create_invite_good() {
// Generation of data
let p = get_pool();
let mut resp = hyper_resp();
// expected params
let params: Value = serde_json::from_str(r#"
{
"uses": 3,
"expire": null
}
"#).unwrap();
// Collection
super::join(&p, &mut resp, params).await;
let _ = p.disconnect().await;
assert_eq!(StatusCode::OK, resp.status());
}
}