Renaming project to json-api for clarity sake
This commit is contained in:
65
json-api/src/admin.rs
Normal file
65
json-api/src/admin.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
// Module deals endpoints pertaining to admin-only actions
|
||||
|
||||
use hyper::{Response, Body};
|
||||
use hyper::StatusCode;
|
||||
|
||||
use mysql_async::Pool;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::perms::ADMIN_PERMS;
|
||||
|
||||
use db::{
|
||||
self,
|
||||
member::Member
|
||||
};
|
||||
|
||||
macro_rules! get_target_id {
|
||||
($obj:expr) => {
|
||||
match $obj.get("target-id") {
|
||||
Some(val) => val.as_u64(),
|
||||
None => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_admin(p: &Pool, response: &mut Response<Body>, params: Value) {
|
||||
// @requires: owner level permission as regular admins can have conflict of interests
|
||||
// @user-param: "target-id": Number
|
||||
|
||||
if let Some(uid) = get_target_id!(params) {
|
||||
|
||||
if let Err(e) = Member::update_perms(p, uid, ADMIN_PERMS).await {
|
||||
eprintln!("{}", e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// this is likely the users fault providing shit ass json
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
*response.body_mut() = Body::from("Missing target user id");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn set_permissions(p: &Pool, response: &mut Response<Body>, params: Value) {
|
||||
// @requiresL: admin level permissions, admins can't touch other admins
|
||||
let tuid = get_target_id!(params);
|
||||
|
||||
let new_perms_opt = match params.get("permissions") {
|
||||
Some(val) => val.as_u64(),
|
||||
None => None
|
||||
};
|
||||
|
||||
match (tuid, new_perms_opt) {
|
||||
(Some(uid), Some(new_perms)) => {
|
||||
// Returns Ok(Response::sucess) | Err(log)
|
||||
if let Err(e) = Member::update_perms(p, uid, new_perms).await {
|
||||
eprintln!("{}", e); // wil be some sql error
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
*response.body_mut() = Body::from("Missing one or more parameters");
|
||||
}
|
||||
}
|
||||
}
|
||||
275
json-api/src/auth.rs
Normal file
275
json-api/src/auth.rs
Normal file
@@ -0,0 +1,275 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use bcrypt::{self, BcryptResult};
|
||||
use mysql_async::Pool;
|
||||
use chrono::{Utc, Duration};
|
||||
|
||||
use crate::routes;
|
||||
|
||||
use db::{member::Member, common::FromDB};
|
||||
use db::Response;
|
||||
|
||||
use jsonwebtoken::EncodingKey;
|
||||
lazy_static! {
|
||||
static ref HMAC_SECRET: Vec<u8> = {
|
||||
std::fs::read("hmac.secret").expect("Couldn't get HMAC secret")
|
||||
};
|
||||
|
||||
static ref ENCODING_KEY: EncodingKey = {
|
||||
EncodingKey::from_secret(&HMAC_SECRET)
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Claim {
|
||||
sub: db::UBigInt, // user id
|
||||
exp: db::BigInt, // expiry date
|
||||
cookie: String, // unique cookie value
|
||||
}
|
||||
|
||||
impl Claim {
|
||||
pub fn new(id: db::UBigInt) -> Claim {
|
||||
Claim {
|
||||
sub: id,
|
||||
exp: Utc::now()
|
||||
.checked_add_signed(Duration::weeks(1))
|
||||
.expect("Couldn't generate an expirey date")
|
||||
.timestamp(),
|
||||
cookie: generate_cookie()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// used when we create a new users for the first time
|
||||
#[derive(Debug)]
|
||||
pub enum AuthReason {
|
||||
Good, //passed regular check
|
||||
OpenAuth, // route does not require auth
|
||||
NoKey, // key missing
|
||||
BadKey, // key is bad
|
||||
LoginValid, // used only to access the login route which is also our refresh
|
||||
ServerIssue(String) // for well 500's
|
||||
}
|
||||
|
||||
|
||||
fn valid_secret(given_pass: &str, hash: &str) -> bool {
|
||||
let result = bcrypt::verify(given_pass, hash);
|
||||
return match result {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_perms(member: Member, path: &str) -> bool {
|
||||
use crate::perms;
|
||||
// if there are perms on the current path make sure the user has them
|
||||
if let Some(p) = perms::get_perm_mask(path) {
|
||||
return (p & member.permissions) == p;
|
||||
}
|
||||
// if no perms then we don't care
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
fn rng_secret(length: usize) -> String {
|
||||
use getrandom::getrandom;
|
||||
use base64::{encode_config, URL_SAFE};
|
||||
|
||||
let mut buf: Vec<u8> = vec![0;length];
|
||||
getrandom(&mut buf).unwrap();
|
||||
encode_config(buf,URL_SAFE)
|
||||
|
||||
}
|
||||
|
||||
pub fn generate_secret() -> String {
|
||||
/*
|
||||
* Generates a url-safe-plaintext secret for our db
|
||||
* */
|
||||
return rng_secret(64);
|
||||
}
|
||||
|
||||
pub fn generate_cookie() -> String {
|
||||
return rng_secret(32)
|
||||
}
|
||||
|
||||
pub fn encrypt_secret(raw: &str) -> BcryptResult<String> {
|
||||
const BCRYPT_COST: u32 = 14;
|
||||
return bcrypt::hash(raw, BCRYPT_COST);
|
||||
}
|
||||
|
||||
fn jwt_from_serde(params: &serde_json::Value) -> Option<&str> {
|
||||
// gets the `token` from the parameters
|
||||
// option<value> -> some(value) -> string
|
||||
return params.get("jwt")?.as_str();
|
||||
}
|
||||
|
||||
async fn valid_jwt(p: &Pool, token: &str) -> AuthReason {
|
||||
use jsonwebtoken::{
|
||||
decode, DecodingKey,
|
||||
Validation, Algorithm
|
||||
};
|
||||
// crypto things that should prolly not fail assuming we're configured correctly
|
||||
let algo = Algorithm::HS512;
|
||||
let dk = DecodingKey::from_secret(&HMAC_SECRET);
|
||||
if let Ok(decoded) = decode::<Claim>(token, &dk, &Validation::new(algo)) {
|
||||
|
||||
// subject used for querying speed NOT security
|
||||
let listed = db::auth::listed_jwt(p, decoded.claims.sub, token).await.unwrap();
|
||||
let active = Utc::now().timestamp() < decoded.claims.exp;
|
||||
|
||||
return match listed && active {
|
||||
true => AuthReason::Good,
|
||||
false => AuthReason::BadKey
|
||||
};
|
||||
}
|
||||
else {
|
||||
return AuthReason::BadKey;
|
||||
}
|
||||
}
|
||||
|
||||
fn login_params_from_serde(params: &serde_json::Value) -> Option<(db::UBigInt, &str)> {
|
||||
let id_v = params.get("id");
|
||||
let secret_v = params.get("secret");
|
||||
return match (id_v, secret_v) {
|
||||
(Some(id_v), Some(secret_v)) => {
|
||||
match (id_v.as_u64(), secret_v.as_str()) {
|
||||
(Some(id), Some(secret)) => Some((id, secret)),
|
||||
_ => None
|
||||
}
|
||||
},
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn wall_entry<'path, 'pool, 'params>(
|
||||
path: &'path str,
|
||||
pool: &'pool Pool,
|
||||
params: &'params serde_json::Value)
|
||||
-> AuthReason {
|
||||
|
||||
// Dont need to auth if it's not required
|
||||
let open_path = routes::is_open(path);
|
||||
let jwt = jwt_from_serde(params);
|
||||
|
||||
if open_path { // ignore the parameters since they're irelevant
|
||||
return AuthReason::OpenAuth;
|
||||
}
|
||||
|
||||
if let Some(jwt) = jwt {
|
||||
// get the headers here
|
||||
return valid_jwt(pool, jwt).await;
|
||||
}
|
||||
if let Some((id, secret)) = login_params_from_serde(params) {
|
||||
// Last chance we might be hitting the /login route so we have to do the heavy auth flow
|
||||
|
||||
if path != routes::AUTH_LOGIN {
|
||||
return AuthReason::BadKey;
|
||||
}
|
||||
else {
|
||||
return match Member::get(pool, id).await {
|
||||
Response::Row(user) => {
|
||||
if valid_secret(secret, &user.secret) && valid_perms(user, path){
|
||||
AuthReason::LoginValid
|
||||
}
|
||||
else {
|
||||
AuthReason::BadKey
|
||||
}
|
||||
},
|
||||
Response::Empty => AuthReason::BadKey,
|
||||
Response::Other(err) => AuthReason::ServerIssue(err),
|
||||
_ => AuthReason::ServerIssue("db-lib returned garbage".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
return AuthReason::NoKey;
|
||||
}
|
||||
|
||||
pub async fn login_get_jwt(p: &Pool, response: &mut hyper::Response<hyper::Body>, params: serde_json::Value) {
|
||||
// basically this route generates a jwt for the user and returns via the jwt key
|
||||
// in the json response
|
||||
use jsonwebtoken::{
|
||||
Header, Algorithm,
|
||||
encode
|
||||
};
|
||||
use hyper::header::HeaderValue;
|
||||
let id = params.get("id").unwrap().as_u64().unwrap(); // only route where we have the "id is there guarantee"
|
||||
let claim = Claim::new(id);
|
||||
let header = Header::new(Algorithm::HS512);
|
||||
let encoded = encode(
|
||||
&header,
|
||||
&claim,
|
||||
&ENCODING_KEY).unwrap();
|
||||
|
||||
match db::auth::add_jwt(p, id, &encoded).await {
|
||||
Ok(_) => {
|
||||
response.headers_mut().insert("Content-Type",
|
||||
HeaderValue::from_static("application/json"));
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"jwt": encoded
|
||||
});
|
||||
*response.body_mut() = hyper::Body::from(payload.to_string());
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
*response.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod auth_tests {
|
||||
use jsonwebtoken::{
|
||||
Header, encode, Algorithm
|
||||
};
|
||||
#[test]
|
||||
fn validity_check() {
|
||||
use bcrypt::{hash, DEFAULT_COST};
|
||||
let plain = super::generate_secret();
|
||||
match hash(&plain, DEFAULT_COST) {
|
||||
Ok(hash) => assert_eq!(super::valid_secret(&plain, &hash), true),
|
||||
Err(err) => panic!("{}", err)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_jwt() {
|
||||
let claim = super::Claim::new(123); // example claim that we send out
|
||||
let header = Header::new(Algorithm::HS512); // header that basically all clients get
|
||||
let encoded = encode(
|
||||
&header,
|
||||
&claim,
|
||||
&super::ENCODING_KEY).unwrap();
|
||||
|
||||
use jsonwebtoken::{decode, DecodingKey, Validation};
|
||||
let dc = decode::<super::Claim>(
|
||||
&encoded,
|
||||
&DecodingKey::from_secret(&super::HMAC_SECRET),
|
||||
&Validation::new(Algorithm::HS512)
|
||||
); // decoding works yet fails on the debugger
|
||||
assert_eq!(dc.is_ok(), true);
|
||||
println!("{:?}", dc);
|
||||
println!("{}", encoded);
|
||||
|
||||
let mut parts:Vec<String> = Vec::new();
|
||||
for i in encoded.split('.') {
|
||||
parts.push(i.to_string());
|
||||
}
|
||||
let head = &parts[0];
|
||||
let claim = &parts[1];
|
||||
let sig = &parts[2];
|
||||
let res = jsonwebtoken::crypto::verify(
|
||||
sig,
|
||||
&format!("{}.{}", head, claim),
|
||||
&DecodingKey::from_secret(&super::HMAC_SECRET),
|
||||
Algorithm::HS512);
|
||||
|
||||
println!("{:?}", res);
|
||||
}
|
||||
}
|
||||
106
json-api/src/channels.rs
Normal file
106
json-api/src/channels.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use hyper::{StatusCode, Response, Body};
|
||||
|
||||
use mysql_async::Pool;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use db::{
|
||||
self,
|
||||
common::FromDB,
|
||||
channels::Channel
|
||||
};
|
||||
|
||||
use crate::http::set_json_body;
|
||||
|
||||
|
||||
pub async fn list_channels(pool: &Pool, response: &mut Response<Body>) {
|
||||
/*
|
||||
* @user-params -> for now none as i don't feel like dealing with it
|
||||
* @TODO: add in a let var that actually
|
||||
*/
|
||||
return match db::channels::Channel::filter(pool, 0).await {
|
||||
db::Response::Set(channels) => {
|
||||
set_json_body(response, json!(channels));
|
||||
},
|
||||
db::Response::Other(_msg) => *response.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
_ => *response.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn create_channel(pool: &Pool, response: &mut Response<Body>, params: Value) {
|
||||
/*
|
||||
* Create a channel base on a few parameters that may or may not be there
|
||||
* @responds with the data of the newly created channel
|
||||
*/
|
||||
// Theres an extra un-needed unwrap to be cut out from this proc
|
||||
// specifically with the desc parameter
|
||||
use std::convert::TryInto;
|
||||
|
||||
let req_params: (Option<&str>, Option<&str>, Option<i64>) =
|
||||
match (params.get("name"), params.get("description"), params.get("kind")) {
|
||||
(Some(name), Some(desc), Some(kind)) => (name.as_str(), desc.as_str(), kind.as_i64()),
|
||||
(Some(name), None, Some(kind)) => (name.as_str(), Some("No Description"), kind.as_i64()),
|
||||
_ => (None, None, None)
|
||||
};
|
||||
|
||||
match req_params {
|
||||
(Some(name), Some(desc), Some(kind)) => {
|
||||
use db::channels::{TEXT_CHANNEL, VOICE_CHANNEL};
|
||||
if kind < VOICE_CHANNEL as i64 || kind > TEXT_CHANNEL as i64 {
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST; // restriciting to 1|2 for valid channel kinds
|
||||
}
|
||||
else {
|
||||
// Send the data up to the db, then return the new channel back to the user(?)
|
||||
match db::channels::Channel::add(pool, name, desc, kind.try_into().unwrap()).await {
|
||||
db::Response::Row(row) => {
|
||||
set_json_body(response, json!(row));
|
||||
},
|
||||
// user error that the db doesn't deal with so we just blame the user
|
||||
db::Response::RestrictedInput(msg) => {
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
set_json_body(response, json!({"error": msg}));
|
||||
},
|
||||
|
||||
// inserted but could not fetch
|
||||
db::Response::Empty => *response.status_mut() = StatusCode::NOT_FOUND,
|
||||
|
||||
//like legit issues past here
|
||||
db::Response::Other(msg) => {
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; // conn issue probably
|
||||
eprintln!("\t[ Channels ] {}", msg);
|
||||
},
|
||||
_ => *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR // ngmi
|
||||
}
|
||||
}
|
||||
},
|
||||
// basically one of the parameter gets failed so we bail on all of this
|
||||
_ => *response.status_mut() = StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_channel(pool: &Pool, response: &mut Response<Body>, params: Value) {
|
||||
// make sure we have the right parameters provided
|
||||
if let Some(name) = params.get("channel_id") {
|
||||
if let Some(id) = name.as_u64() {
|
||||
// TODO: something more intelligent with the logging im ngl
|
||||
match Channel::delete(pool, id).await {
|
||||
db::Response::Success => {},
|
||||
db::Response::Other(data) => {
|
||||
eprintln!("\t{}", data);
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
_ => {
|
||||
eprintln!("\tBro like restart the server");
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
}
|
||||
}
|
||||
else {
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
37
json-api/src/http.rs
Normal file
37
json-api/src/http.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use serde_json::{self, Value};
|
||||
use hyper::http::HeaderValue;
|
||||
use hyper::Response;
|
||||
use hyper::Body;
|
||||
use hyper::body::to_bytes;
|
||||
|
||||
use std::u8;
|
||||
|
||||
const APP_JSON_HEADER: &'static str = "application/json";
|
||||
const CONTENT_TYPE: &'static str = "Content-Type";
|
||||
|
||||
pub fn set_json_body(response: &mut Response<Body>, values: Value) {
|
||||
response.headers_mut().insert(
|
||||
CONTENT_TYPE,
|
||||
HeaderValue::from_static(APP_JSON_HEADER));
|
||||
|
||||
*response.body_mut() = Body::from(values.to_string());
|
||||
}
|
||||
|
||||
pub async fn parse_json_params(body_raw: &mut Body) -> Result<Value, serde_json::error::Error> {
|
||||
let bytes: &[u8] = &*to_bytes(body_raw).await.unwrap(); // rarely fails
|
||||
let values: Value;
|
||||
if bytes.len() == 0 {
|
||||
values = serde_json::from_str("{}")?;
|
||||
}
|
||||
else {
|
||||
values = serde_json::from_slice(bytes)?;
|
||||
}
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn extract_uid(values: &Value) -> u64 {
|
||||
// pulling 'id' from user params is safe because the
|
||||
// auth modules guarantees this to be there already
|
||||
values.get("id").unwrap().as_u64().unwrap()
|
||||
}
|
||||
2
json-api/src/http_params.rs
Normal file
2
json-api/src/http_params.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
|
||||
218
json-api/src/invites.rs
Normal file
218
json-api/src/invites.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
243
json-api/src/main.rs
Normal file
243
json-api/src/main.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
extern crate db;
|
||||
extern crate chrono;
|
||||
extern crate clap;
|
||||
extern crate dotenv;
|
||||
extern crate getrandom;
|
||||
extern crate bcrypt;
|
||||
extern crate base64;
|
||||
extern crate serde;
|
||||
extern crate jsonwebtoken;
|
||||
#[macro_use] extern crate lazy_static;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::convert::Infallible; // our main dispatcher basically never fails hence why we use this
|
||||
use std::env::{self, set_var};
|
||||
|
||||
use tokio;
|
||||
use hyper::{
|
||||
self,
|
||||
Server,
|
||||
Response, Request, Body,
|
||||
Method, StatusCode,
|
||||
service::{make_service_fn, service_fn}
|
||||
};
|
||||
use mysql_async::Pool;
|
||||
|
||||
use dotenv::dotenv;
|
||||
use clap::{Arg, App};
|
||||
use auth::AuthReason;
|
||||
|
||||
mod auth;
|
||||
|
||||
mod routes;
|
||||
mod meta;
|
||||
mod invites;
|
||||
mod channels;
|
||||
mod members;
|
||||
mod perms;
|
||||
mod messages;
|
||||
mod admin;
|
||||
|
||||
mod http;
|
||||
mod testing;
|
||||
|
||||
const NO_ERR: u16 = 0;
|
||||
const CONFIG_ERR: u16 = 1;
|
||||
const SHUTDOWN_ERR: u16 = 2;
|
||||
|
||||
async fn route_dispatcher(pool: &Pool, resp: &mut Response<Body>, meth: &Method, path: &str, params: serde_json::Value) {
|
||||
// At some point we should have some way of hiding this obnoxious complexity
|
||||
const GET: &Method = &Method::GET;
|
||||
const POST: &Method = &Method::POST;
|
||||
const DELETE: &Method = &Method::DELETE;
|
||||
match (meth, path) {
|
||||
/* INVITES */
|
||||
(GET, routes::INVITE_CREATE) => invites::create(pool, resp, params).await,
|
||||
(GET, routes::INVITE_JOIN) => invites::join(pool, resp, params).await,
|
||||
/* CHANNELS */
|
||||
(GET, routes::CHANNELS_LIST) => channels::list_channels(pool, resp).await,
|
||||
(POST, routes::CHANNELS_CREATE) => channels::create_channel(pool, resp, params).await,
|
||||
(DELETE, routes::CHANNELS_DELETE) => channels::delete_channel(pool, resp, params).await,
|
||||
/* MESSAGING */
|
||||
(POST, routes::MESSAGE_SEND) => messages::send_message(pool, resp, params).await,
|
||||
(GET, routes::MESSAGE_TIME_RANGE) => messages::get_by_time(pool, resp, params).await,
|
||||
(GET, routes::MESSAGE_FROM_ID) =>messages::from_id(pool, resp, params).await,
|
||||
/* ADMIN */
|
||||
(POST, routes::SET_PERMS_BY_ADMIN) => admin::set_permissions(pool, resp, params).await,
|
||||
/* MEMBERS */
|
||||
(GET, routes::GET_ONLINE_MEMBERS) => members::get_online_members(pool, resp).await,
|
||||
/* OWNER */
|
||||
(POST, routes::SET_NEW_ADMIN) => admin::new_admin(pool, resp, params).await,
|
||||
/* META ROUTE */
|
||||
(GET, routes::META) => meta::server_meta(resp).await,
|
||||
_ => {
|
||||
eprintln!("\tNOT FOUND: {}: {}", meth, path);
|
||||
*resp.status_mut() = StatusCode::NOT_FOUND
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn main_responder(request: Request<Body>) -> Result<Response<Body>, hyper::Error>{
|
||||
use AuthReason::*;
|
||||
let mut response = Response::new(Body::empty());
|
||||
|
||||
let (parts, mut body) = request.into_parts();
|
||||
let method = parts.method;
|
||||
let path = parts.uri.path();
|
||||
|
||||
let params_res = http::parse_json_params(&mut body).await;
|
||||
|
||||
if let Ok(params) = params_res {
|
||||
let mysql_pool = Pool::new(&env::var("DATABASE_URL").unwrap());
|
||||
match auth::wall_entry(path, &mysql_pool, ¶ms).await {
|
||||
OpenAuth | Good => route_dispatcher(&mysql_pool, &mut response, &method, path, params).await,
|
||||
LoginValid => auth::login_get_jwt(&mysql_pool, &mut response, params).await,
|
||||
NoKey | BadKey => *response.status_mut() = StatusCode::UNAUTHORIZED,
|
||||
ServerIssue(msg) => {
|
||||
println!("\tAUTH : 500 [{}]", msg);
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
println!("\tPARSER: Parameter parsing failed");
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("Failed to capture ctrl-c signal");
|
||||
}
|
||||
|
||||
async fn start_server(ecode: u16) -> u16 {
|
||||
println!("Servering on localhost:8888");
|
||||
let addr = SocketAddr::from(([127,0,0,1], 8888));
|
||||
let service = make_service_fn(|_conn| async {
|
||||
Ok::<_, Infallible>(service_fn(main_responder))
|
||||
});
|
||||
let server = Server::bind(&addr).serve(service);
|
||||
let graceful_shutdown = server.with_graceful_shutdown(shutdown_signal());
|
||||
|
||||
if let Err(e) = graceful_shutdown.await {
|
||||
eprintln!("Server shutdown error: {}", e);
|
||||
return ecode | SHUTDOWN_ERR;
|
||||
}
|
||||
else {
|
||||
return ecode
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async fn attempt_owner_creation(name: &str) {
|
||||
/*
|
||||
* Attempts to create an owner level account 'name' as the name
|
||||
* Writes succesful output to stdout
|
||||
* Writes error output to stderr
|
||||
* NOTE: Messy because there's 0 other places where this kind of direct
|
||||
* functionality is required. db-lib is basically built to talk to the api
|
||||
* */
|
||||
let p = Pool::new(&env::var("DATABASE_URL").unwrap());
|
||||
let owner_secret = auth::generate_secret();
|
||||
if let Ok(enc_secret) = auth::encrypt_secret(&owner_secret) {
|
||||
if let Ok(response) = db::member::Member::add(&p, name, &enc_secret, perms::OWNER).await {
|
||||
match response {
|
||||
db::Response::Row(mut owner) => {
|
||||
owner.secret = owner_secret; // giving the secret itself back to the user
|
||||
println!("{}", serde_json::to_string(&owner).expect("SQL query passed but serde couldn't parse the data for some reason"))
|
||||
},
|
||||
db::Response::Empty => {
|
||||
eprintln!("SQL server failed to return owner data, check configs and also the members table to make sure there's nothing there by accident");
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
else {
|
||||
eprintln!("Could not communicate with the SQL server, check your configs!");
|
||||
}
|
||||
}
|
||||
else {
|
||||
eprintln!("Could not generate a proper secret");
|
||||
}
|
||||
p.disconnect();
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), u16>{
|
||||
|
||||
let mut main_ret: u16 = 0;
|
||||
let d_result = dotenv();
|
||||
|
||||
// check for a database_url before the override we get from the cmd line
|
||||
if let Err(_d) = d_result {
|
||||
if let Err(_e) = env::var("DATABASE_URL") {
|
||||
main_ret |= CONFIG_ERR;
|
||||
}
|
||||
}
|
||||
|
||||
let args = App::new("Freechat Server")
|
||||
.version("0.1")
|
||||
.author("shockrah")
|
||||
.about("Decentralized chat system")
|
||||
.arg(Arg::with_name("db-url")
|
||||
.short("d")
|
||||
.long("db-url")
|
||||
.value_name("DATABASE URL")
|
||||
.help("Sets the DATABASE URL via an environment variable")
|
||||
.takes_value(true))
|
||||
.arg(Arg::with_name("create-owner")
|
||||
.short("c")
|
||||
.long("create-owner")
|
||||
.value_name("Owner")
|
||||
.help("Creates an account with full permissions in the SQL database."))
|
||||
.arg(Arg::with_name("server")
|
||||
.short("s")
|
||||
.long("server")
|
||||
.help("Starts the API server"))
|
||||
.get_matches();
|
||||
|
||||
|
||||
if args.args.len() == 0 {
|
||||
println!("Freechat Server 0.1
|
||||
shockrah
|
||||
Decentralized chat system
|
||||
|
||||
USAGE:
|
||||
freechat-server [FLAGS] [OPTIONS]
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
-s, --server Starts the API server
|
||||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
-c, --create-owner <Owner> Creates an account with full permissions in the SQL database.
|
||||
-d, --db-url <DATABASE URL> Sets the DATABASE URL via an environment variable");
|
||||
}
|
||||
|
||||
if let Some(db_url) = args.value_of("db-url") {
|
||||
set_var("DATABASE_URL", db_url);
|
||||
}
|
||||
|
||||
if let Some(owner_name) = args.value_of("create-owner") {
|
||||
attempt_owner_creation(owner_name).await;
|
||||
}
|
||||
|
||||
if args.is_present("server") {
|
||||
if main_ret == NO_ERR {
|
||||
main_ret = start_server(main_ret).await;
|
||||
}
|
||||
}
|
||||
|
||||
if main_ret != 0 {
|
||||
// dumb as heck loggin method here
|
||||
if main_ret & CONFIG_ERR != 0 {println!("ERROR: Config was not setup properly => Missing {{DATABASE_URL}}");}
|
||||
if main_ret & SHUTDOWN_ERR != 0 {println!("ERROR: Couldn't shutdown gracefully");}
|
||||
Err(main_ret)
|
||||
}
|
||||
else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
21
json-api/src/members.rs
Normal file
21
json-api/src/members.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use hyper::{Response, Body, StatusCode};
|
||||
use mysql_async::Pool;
|
||||
use serde_json::json;
|
||||
|
||||
use db::member::STATUS_ONLINE;
|
||||
use db::common::FromDB;
|
||||
|
||||
use crate::http::set_json_body;
|
||||
|
||||
pub async fn get_online_members(p: &Pool, response: &mut Response<Body>) {
|
||||
// TODO: at some point we should provide a way of not querying literally every user in
|
||||
// existance
|
||||
// TODO: loggin at some point or something idklol
|
||||
return match db::channels::Channel::filter(p, STATUS_ONLINE).await {
|
||||
db::Response::Set(users) => {
|
||||
set_json_body(response, json!(users));
|
||||
},
|
||||
db::Response::Other(_msg) => *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR,
|
||||
_ => *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
}
|
||||
194
json-api/src/messages.rs
Normal file
194
json-api/src/messages.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use mysql_async::Pool;
|
||||
use hyper::{Response, Body, StatusCode};
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::http::{self, set_json_body};
|
||||
use db::messages::Message;
|
||||
|
||||
pub async fn get_by_time(pool: &Pool, response: &mut Response<Body>, params: Value) {
|
||||
/*
|
||||
* Has a ton of required parameters just be warned
|
||||
* @channel: channel id we're looking at
|
||||
* @start-time: how long ago do we start searching
|
||||
* @end-time: how long ago do we stop searching
|
||||
* {
|
||||
* "channel": 1,
|
||||
* "start-time": unix_now - 24 hours
|
||||
* "end-time": unix_now - 23 hours
|
||||
* }
|
||||
*
|
||||
*/
|
||||
let channel = match params.get("channel") {
|
||||
Some(chan_v) => chan_v.as_u64(),
|
||||
None => None
|
||||
};
|
||||
let start_time = match params.get("start-time") {
|
||||
Some(val) => val.as_i64(),
|
||||
None => None
|
||||
};
|
||||
let end_time = match params.get("end-time") {
|
||||
Some(val) => val.as_i64(),
|
||||
None => None
|
||||
};
|
||||
let limit = match params.get("limit") {
|
||||
Some(val) => val.as_u64(),
|
||||
None => None
|
||||
};
|
||||
|
||||
// TODO: flatten me mommy
|
||||
if let (Some(channel), Some(start), Some(end)) = (channel, start_time, end_time) {
|
||||
match Message::get_time_range(pool, channel, start, end, limit).await {
|
||||
Ok(db_response) => {
|
||||
match db_response {
|
||||
// this absolute lack of data streaming is prolly gonna suck like
|
||||
// a whore in hell week for performance but lets pretend servers don't get massive
|
||||
db::Response::Set(messages) => {
|
||||
set_json_body(response, json!({"messages": messages}));
|
||||
},
|
||||
db::Response::RestrictedInput(_/*error message to log*/) => {
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
}
|
||||
_ => {
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
};
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_message(pool: &Pool, response: &mut Response<Body>, params: Value) {
|
||||
/*
|
||||
* @content: expecting string type
|
||||
* @channel: channel id that we're going to send a message to
|
||||
*/
|
||||
// NOTE: auth module guarantees this will be there in the correct form
|
||||
let author = http::extract_uid(¶ms);
|
||||
|
||||
match (params.get("content") , params.get("channel")) {
|
||||
(Some(content_v), Some(channel_id_v)) => {
|
||||
let (content, channel) = (content_v.as_str(), channel_id_v.as_u64());
|
||||
|
||||
if let (Some(message), Some(cid)) = (content, channel) {
|
||||
// call returns empty on sucess so we don't need to do anything
|
||||
// TODO: loggin
|
||||
let db_result = db::messages::Message::send(pool, message, cid, author).await;
|
||||
if let Err(issue) = db_result {
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
eprintln!("\t{}", issue);
|
||||
}
|
||||
else {
|
||||
match db_result.unwrap() {
|
||||
db::Response::RestrictedInput(msg) => {
|
||||
// user issue
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
set_json_body(response, json!({"msg": msg}))
|
||||
},
|
||||
db::Response::Empty => {}, // nothing to do hyper defaults to 200
|
||||
db::Response::Other(msg) => {
|
||||
eprintln!("{}", msg);
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
},
|
||||
_ => *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR
|
||||
};
|
||||
}
|
||||
}
|
||||
else {
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
}
|
||||
},
|
||||
_ => *response.status_mut() = StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_id(pool: &Pool, response: &mut Response<Body>, params: Value) {
|
||||
/*
|
||||
* @start-id: u64
|
||||
* @limit: optional<u64>
|
||||
* @channel: u64
|
||||
* {
|
||||
* "channel": 1,
|
||||
* "start": 123,
|
||||
* "limit": 100
|
||||
* }
|
||||
*/
|
||||
let channel = match params.get("channel") {
|
||||
Some(chan_v) => chan_v.as_u64(),
|
||||
None => None
|
||||
};
|
||||
let start_id = match params.get("start") {
|
||||
Some(val) => val.as_u64(),
|
||||
None => None
|
||||
};
|
||||
let limit = match params.get("limit") {
|
||||
Some(val) => val.as_u64(),
|
||||
None => None
|
||||
};
|
||||
|
||||
if let (Some(channel), Some(start_id)) = (channel, start_id) {
|
||||
match Message::get_from_id(pool, channel, start_id, limit).await {
|
||||
Ok(db_response) => {
|
||||
match db_response {
|
||||
db::Response::Set(messages) => {
|
||||
// NOTE this check is here because the db's check doesn't
|
||||
// correctly with async and caching and magic idfk its here
|
||||
// it works its correct and the cost is the same as putting
|
||||
// it in the db layer so whatever
|
||||
if messages.len() == 0 {
|
||||
*response.status_mut() = StatusCode::NOT_FOUND;
|
||||
}
|
||||
else {
|
||||
set_json_body(response, json!({"messages": messages}));
|
||||
}
|
||||
},
|
||||
|
||||
_ => *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR
|
||||
};
|
||||
},
|
||||
Err(err) => {
|
||||
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
eprintln!("{}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod messaging_tests {
|
||||
use crate::testing::{get_pool, hyper_resp};
|
||||
use serde_json::Value;
|
||||
use hyper::StatusCode;
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_message_test_missing_channel() {
|
||||
/*
|
||||
* Attempt to send a message i na channel that does not exist
|
||||
*/
|
||||
let p = get_pool();
|
||||
let mut resp = hyper_resp();
|
||||
|
||||
let params: Value = serde_json::from_str(r#"
|
||||
{
|
||||
"channel": "this does not exist",
|
||||
"content": "bs message",
|
||||
"id": 420
|
||||
}
|
||||
"#).unwrap();
|
||||
|
||||
super::send_message(&p, &mut resp, params).await;
|
||||
|
||||
assert_ne!(StatusCode::OK, resp.status());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
25
json-api/src/meta.rs
Normal file
25
json-api/src/meta.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
// Basic handler for getting meta data about the server
|
||||
use std::env::var;
|
||||
|
||||
use hyper::{Response, Body};
|
||||
use serde_json::to_string;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive( Serialize)]
|
||||
struct Config {
|
||||
name: String,
|
||||
description: String,
|
||||
url: String,
|
||||
port: u16
|
||||
}
|
||||
|
||||
|
||||
pub async fn server_meta(response: &mut Response<Body>) {
|
||||
let payload = Config {
|
||||
name: var("SERVER_NAME").unwrap_or("No name".into()),
|
||||
description: var("SERVER_DESCRIPTION").unwrap_or("No description".into()),
|
||||
url: var("SERVER_URL").expect("Couldn't get url from environment"),
|
||||
port: var("SERVER_PORT").expect("Couldn't get port from environment").parse::<u16>().unwrap(),
|
||||
};
|
||||
*response.body_mut() = Body::from(to_string(&payload).unwrap());
|
||||
}
|
||||
36
json-api/src/perms.rs
Normal file
36
json-api/src/perms.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
// GENERAL PERMISSIONS
|
||||
pub const JOIN_VOICE:u64 = 1;
|
||||
pub const SEND_MESSAGES:u64 = 2;
|
||||
pub const CHANGE_NICK:u64 = 16;
|
||||
pub const ALLOW_PFP:u64 = 32;
|
||||
pub const CREATE_TMP_INVITES:u64 = 4;
|
||||
pub const CREATE_PERM_INVITES:u64 = 8; // to make perma invites you need both flags
|
||||
|
||||
|
||||
pub const _ADMIN: u64 = 1 << 62; // can make other admins but can't really touch the owner
|
||||
|
||||
// ADMIN PERMS
|
||||
pub const CREATE_CHANNEL:u64 = 64;
|
||||
pub const DELETE_CHANNEL:u64 = 128;
|
||||
|
||||
// BELOW ARE COLLECTIVE PERMISSION SETS
|
||||
pub const OWNER: u64 = std::u64::MAX;
|
||||
pub const GENERAL_NEW: u64 = JOIN_VOICE | SEND_MESSAGES | ALLOW_PFP | CHANGE_NICK;
|
||||
pub const ADMIN_PERMS: u64 = !(std::u64::MAX & OWNER); // filter the only perm admins don't get
|
||||
|
||||
pub fn get_perm_mask(path: &str) -> Option<u64> {
|
||||
use crate::routes::{
|
||||
INVITE_CREATE,
|
||||
CHANNELS_LIST, CHANNELS_CREATE, CHANNELS_DELETE,
|
||||
MESSAGE_SEND,
|
||||
};
|
||||
match path {
|
||||
INVITE_CREATE => Some(CREATE_TMP_INVITES),
|
||||
CHANNELS_LIST => None,
|
||||
CHANNELS_CREATE => Some(CREATE_CHANNEL),
|
||||
CHANNELS_DELETE => Some(DELETE_CHANNEL),
|
||||
MESSAGE_SEND => Some(SEND_MESSAGES),
|
||||
_ => Some(0)
|
||||
}
|
||||
}
|
||||
27
json-api/src/routes.rs
Normal file
27
json-api/src/routes.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
type Rstr = &'static str;
|
||||
|
||||
pub const AUTH_LOGIN: Rstr = "/login"; // requires @id @secret
|
||||
|
||||
pub const META: Rstr = "/meta"; // @ perms none @ requires JWT however
|
||||
pub const INVITE_CREATE: Rstr = "/invite/create"; // @ perms::CREATE_INVITE
|
||||
pub const INVITE_JOIN: Rstr = "/join"; // @ none for new accounts
|
||||
|
||||
pub const CHANNELS_LIST: Rstr = "/channels/list"; // requires none
|
||||
pub const CHANNELS_CREATE: Rstr = "/channels/create"; // requires @name @kind perms::CREATE_CHANNEl
|
||||
pub const CHANNELS_DELETE: Rstr = "/channels/delete"; // requires @name perms::DELETE_CHANNEL
|
||||
|
||||
pub const MESSAGE_SEND: Rstr = "/message/send"; // requires @content perms::MESSAGE_SEND
|
||||
pub const MESSAGE_TIME_RANGE: Rstr = "/message/get_range"; // requires @channel(id) @start-time @end-time
|
||||
pub const MESSAGE_FROM_ID: Rstr = "/message/from_id"; // requires @channel(id) requires @start(id) @<optional>limit(1..1000)
|
||||
|
||||
pub const GET_ONLINE_MEMBERS: Rstr = "/members/get_online";
|
||||
|
||||
|
||||
// ADMIN ROUTES
|
||||
pub const SET_PERMS_BY_ADMIN: Rstr = "/admin/setpermisions"; // @requires perms::ADMIN
|
||||
pub const SET_NEW_ADMIN: Rstr = "/owner/newadmin"; // @requiers: owner perms
|
||||
|
||||
pub fn is_open(path: &str) -> bool {
|
||||
return path.starts_with("/join") || path.starts_with("/meta");
|
||||
}
|
||||
|
||||
54
json-api/src/schema.rs
Normal file
54
json-api/src/schema.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
table! {
|
||||
channels (id) {
|
||||
id -> Unsigned<Bigint>,
|
||||
name -> Varchar,
|
||||
description -> Nullable<Varchar>,
|
||||
kind -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
invites (id) {
|
||||
id -> Bigint,
|
||||
uses -> Nullable<Bigint>,
|
||||
expires -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
jwt (id) {
|
||||
id -> Unsigned<Bigint>,
|
||||
token -> Varchar,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
members (id, secret) {
|
||||
id -> Unsigned<Bigint>,
|
||||
secret -> Varchar,
|
||||
name -> Varchar,
|
||||
joindate -> Bigint,
|
||||
status -> Integer,
|
||||
permissions -> Unsigned<Bigint>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
messages (id) {
|
||||
id -> Unsigned<Bigint>,
|
||||
time -> Bigint,
|
||||
content -> Varchar,
|
||||
author_id -> Unsigned<Bigint>,
|
||||
channel_id -> Unsigned<Bigint>,
|
||||
}
|
||||
}
|
||||
|
||||
joinable!(messages -> channels (channel_id));
|
||||
|
||||
allow_tables_to_appear_in_same_query!(
|
||||
channels,
|
||||
invites,
|
||||
jwt,
|
||||
members,
|
||||
messages,
|
||||
);
|
||||
20
json-api/src/testing/mod.rs
Normal file
20
json-api/src/testing/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
// Functions which are only really useful for the unit tests but which show up
|
||||
// constantly in the tests themselves
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn get_pool() -> mysql_async::Pool {
|
||||
use dotenv::dotenv;
|
||||
use mysql_async::Pool;
|
||||
|
||||
dotenv().ok();
|
||||
return Pool::new(&std::env::var("DATABASE_URL").unwrap())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn hyper_resp() -> hyper::Response<hyper::Body> {
|
||||
use hyper::{Response, Body};
|
||||
|
||||
Response::new(Body::empty())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user