Renaming project to json-api for clarity sake
This commit is contained in:
14
json-api/.env
Normal file
14
json-api/.env
Normal file
@@ -0,0 +1,14 @@
|
||||
DATABASE_URL=mysql://freechat_dev:password@localhost:3306/freechat
|
||||
DATABASE_NAME=freechat
|
||||
DATABASE_PASS=password
|
||||
DATABASE_USER=freechat_dev
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_PORT=3306
|
||||
REDIS_URL=redis://127.0.0.1:6379
|
||||
|
||||
|
||||
# Server meta things
|
||||
SERVER_NAME="Freechat Dev Server"
|
||||
SERVER_DESCRIPTION="Server for sick development things"
|
||||
SERVER_URL=localhost
|
||||
SERVER_PORT=8888
|
||||
12
json-api/.gitignore
vendored
Normal file
12
json-api/.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
target
|
||||
**/*.rs.bk
|
||||
|
||||
static/css/
|
||||
dev-sql/
|
||||
diesel.toml
|
||||
|
||||
*.log
|
||||
|
||||
# Client pycache
|
||||
client-tests/__pycache__/
|
||||
client-tests/web/__pycache__/
|
||||
25
json-api/.vscode/launch.json
vendored
Normal file
25
json-api/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Client Tests",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/client-tests/client.py",
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "(Linux) Debug",
|
||||
"program": "${workspaceFolder}/target/debug/freechat-server",
|
||||
"args": [
|
||||
"-s"
|
||||
],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
json-api/.vscode/settings.json
vendored
Normal file
3
json-api/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.pythonPath": "/home/shockrah/GitRepos/freechat/server-api/client-tests/bin/python3.9"
|
||||
}
|
||||
2218
json-api/Cargo.lock
generated
Normal file
2218
json-api/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
json-api/Cargo.toml
Normal file
36
json-api/Cargo.toml
Normal file
@@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "json-api"
|
||||
version = "0.2.0"
|
||||
authors = ["shockrah <alejandros714@protonmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[workspace]
|
||||
[dependencies]
|
||||
tokio = { version = "0.2", features=["full"] }
|
||||
hyper = "0.13"
|
||||
futures = "0.3"
|
||||
url = "2.1.1"
|
||||
mysql_async = "0.23.1"
|
||||
|
||||
dotenv = "0.9.0"
|
||||
|
||||
chrono = "0.4.0"
|
||||
time = "0.2"
|
||||
|
||||
getrandom = "0.1"
|
||||
bcrypt = "0.8"
|
||||
base64 = "0.12.1"
|
||||
rand = "0.7.3"
|
||||
jsonwebtoken = "7.2.0"
|
||||
|
||||
clap = "2.32.2"
|
||||
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0.114", features = [ "derive" ] }
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
# Database library/interface
|
||||
db = { path = "db" }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.2.1"
|
||||
45
json-api/build.sh
Executable file
45
json-api/build.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
_help() {
|
||||
cat <<EOF
|
||||
Usage: ./build.sh [-h|-t|-b|-r]
|
||||
h Help : shows this command
|
||||
f Full : Runs all available tests including the cargo ones
|
||||
t Test : Runs All tests from cargo tests to client tests
|
||||
T Test Realse : Runs tests against release build
|
||||
b Build : Builds dev build with 'cargo build'
|
||||
r Release : Builds release build with --release flag
|
||||
EOF
|
||||
}
|
||||
|
||||
[[ -z $1 ]] && _help && exit 0
|
||||
|
||||
diesel database reset
|
||||
export CARGO_BIN=$HOME/.cargo/bin/cargo
|
||||
full=
|
||||
|
||||
testing() {
|
||||
release_opt=$1
|
||||
cargo build $release_opt
|
||||
cargo run $release_opt -- -s &
|
||||
server=$!
|
||||
echo Waiting on server to spin up && sleep 2
|
||||
|
||||
python3 client-tests/client.py
|
||||
kill -9 $server 2> /dev/null
|
||||
if [ ! -z $full ];then
|
||||
cargo test $release_opt
|
||||
fi
|
||||
}
|
||||
|
||||
while getopts ":fhtTbr" arg; do
|
||||
case ${arg} in
|
||||
f) full=true;;
|
||||
h) echo help command;;
|
||||
t) testing;;
|
||||
T) testing --release;;
|
||||
b) cargo build;;
|
||||
r) cargo build --release;;
|
||||
*) _help;;
|
||||
esac
|
||||
done
|
||||
2
json-api/client-tests/.gitignore
vendored
Normal file
2
json-api/client-tests/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
bin/
|
||||
lib/
|
||||
1
json-api/client-tests/__init__.py
Normal file
1
json-api/client-tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import web
|
||||
220
json-api/client-tests/client.py
Normal file
220
json-api/client-tests/client.py
Normal file
@@ -0,0 +1,220 @@
|
||||
import time
|
||||
import subprocess, os, sys
|
||||
import json, requests
|
||||
from web import http
|
||||
|
||||
|
||||
class Worker:
|
||||
def __init__(self, domain: str, create_admin: bool):
|
||||
'''
|
||||
@opt:base = base url string
|
||||
@opt:create_admin = creates admin account directly off cargo -c <NAME>
|
||||
@opt:admin = optionally pass in dictionary with some admin credentials
|
||||
potentially from another instance to run multiple tests at once
|
||||
'''
|
||||
|
||||
self.domain = domain
|
||||
self.requests = {}
|
||||
self.responses = {}
|
||||
self.body_logs = {}
|
||||
|
||||
self.jwt = None # never gets assigned until /login is hit
|
||||
self.basic_creds = self.__create_admin()
|
||||
self.id = self.basic_creds['id']
|
||||
self.secret = self.basic_creds['secret']
|
||||
|
||||
def __create_admin(self):
|
||||
# /home/$user/.cargo/bin/cargo <- normally
|
||||
# NOTE: the above path is prolly some awful shit on ci pipeline
|
||||
CARGO_BIN = os.getenv('CARGO_BIN')
|
||||
|
||||
proc = subprocess.run(f'cargo run --release -- -c dev-test'.split(), text=True, capture_output=True)
|
||||
try:
|
||||
return json.loads(proc.stdout)
|
||||
except:
|
||||
import sys
|
||||
print('TESTBOT UNABLE TO LOAD JSON DATA FROM -c flag', file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
def auth(self, auth_opt: str):
|
||||
if auth_opt == 'basic':
|
||||
return self.basic_creds
|
||||
else:
|
||||
return {'id': self.id, 'jwt': auth_opt}
|
||||
|
||||
|
||||
def _append_auth(self, opts: dict, auth: str):
|
||||
'''
|
||||
Default auth fallback type is jwt because that's they only other type of auth
|
||||
FC cares about really
|
||||
|
||||
@param opts: Dictionary of parameters to pass to the endpoint
|
||||
@auth: Denotes if we use basic auth or jwt if its not 'basic'
|
||||
|
||||
'''
|
||||
if type(auth) == str:
|
||||
opts['id'] = self.id
|
||||
if auth == 'basic':
|
||||
opts['secret'] = self.secret
|
||||
else:
|
||||
opts['jwt'] = auth
|
||||
return opts
|
||||
else:
|
||||
# if its not a string we don't add anything in
|
||||
return opts
|
||||
|
||||
def logs(self):
|
||||
ids = sorted(self.requests.keys()) # shared keys in requests/responses
|
||||
for key in ids:
|
||||
self.responses[key].log()
|
||||
|
||||
# Logg the provided data to ensure that _it_ wasn't the cause for error
|
||||
resp = self.responses[key]
|
||||
if resp.code != resp.expected:
|
||||
opts = self.requests[key].params
|
||||
print(f'\tParams: {opts}')
|
||||
if self.body_logs[key] is True:
|
||||
print(f'\tBody: {self.responses[key].body}')
|
||||
|
||||
|
||||
def request(self, method: str, path: str, auth: str, opts: dict, expectation: int, show_body=False):
|
||||
assert(path[0] == '/')
|
||||
|
||||
# First make sure we add in the correct auth params that are requested
|
||||
opts = self._append_auth(opts, auth)
|
||||
|
||||
# Build the request and store it in our structure
|
||||
url = self.domain + path
|
||||
req = http.Request(method, url, opts)
|
||||
r_id = time.time()
|
||||
|
||||
resp = req.make(expectation)
|
||||
|
||||
# update log trackers
|
||||
self.requests[r_id] = req
|
||||
self.responses[r_id] = resp
|
||||
self.body_logs[r_id] = show_body
|
||||
|
||||
return r_id
|
||||
|
||||
|
||||
def run(worker: Worker):
|
||||
VOICE_CHAN = 1
|
||||
TEXT_CHAN = 2
|
||||
# Basically every test requires a jwt to be passed in so we grab that here
|
||||
# Should this fail so should nearly every other test from this point
|
||||
req_login = worker.request('post', '/login', 'basic',{}, 200)
|
||||
jwt = worker.responses[req_login].json()['jwt']
|
||||
|
||||
new_channel_name = time.time()
|
||||
|
||||
channel_tests = [
|
||||
# sanity check
|
||||
{'init': ['get', '/channels/list', {}], 'auth': None, 'hope': 401},
|
||||
|
||||
{'init': ['post', '/channels/list', {}], 'auth': jwt, 'hope': 404},
|
||||
|
||||
{'init': ['post', '/channels/create', {'name': str(new_channel_name), 'kind': TEXT_CHAN, 'description': 'asdf'}], 'auth': jwt, 'hope': 200},
|
||||
# Just a regular test no saving for this one
|
||||
{'init': ['post', '/channels/create', {'name': str(new_channel_name+1), 'kind': TEXT_CHAN,}], 'auth': jwt, 'hope': 200},
|
||||
|
||||
{'init': ['post', '/channels/create', {}], 'auth': jwt, 'hope': 400},
|
||||
{'init': ['post', '/channels/create', {'name': 123, 'kind': 'adsf'}], 'auth': jwt, 'hope': 400},
|
||||
|
||||
# save this and compare its results to the previous
|
||||
{'init': ['get', '/channels/list', {}], 'auth': jwt, 'hope': 200},
|
||||
{'init': ['get', '/channels/list', {'random-param': 123}], 'auth': jwt, 'hope': 200},
|
||||
]
|
||||
|
||||
for test in channel_tests:
|
||||
method, path, opts = test['init']
|
||||
auth = test['auth']
|
||||
hope = test['hope']
|
||||
|
||||
worker.request(method, path, auth, opts, hope)
|
||||
|
||||
msg_chan_name = time.time()
|
||||
_id = worker.request('post', '/channels/create', jwt, {
|
||||
'name': str(msg_chan_name),
|
||||
'kind': TEXT_CHAN,
|
||||
}, 200)
|
||||
chan_d = worker.responses[_id].json()
|
||||
|
||||
message_tests = [
|
||||
# bs message spam
|
||||
{'init': ['post', '/message/send', {'channel': chan_d['id'], 'content': 'bs content'}], 'auth': jwt, 'hope': 200},
|
||||
{'init': ['post', '/message/send', {'channel': chan_d['id'], 'content': 'bs content'}], 'auth': jwt, 'hope': 200},
|
||||
{'init': ['post', '/message/send', {'channel': chan_d['id'], 'content': 'bs content'}], 'auth': jwt, 'hope': 200},
|
||||
{'init': ['post', '/message/send', {'channel': chan_d['id'], 'content': 'bs content'}], 'auth': jwt, 'hope': 200},
|
||||
|
||||
# can we get them back tho?
|
||||
{
|
||||
'init': [
|
||||
'get', '/message/get_range', {'channel': chan_d['id'], 'start-time': int(msg_chan_name-10), 'end-time': int(msg_chan_name + 10)}
|
||||
],
|
||||
'auth': jwt, 'hope': 200
|
||||
},
|
||||
{
|
||||
'init': [
|
||||
'get', '/message/get_range', {'channel': chan_d['id'], 'end-time': int(msg_chan_name)}
|
||||
],
|
||||
'auth': jwt, 'hope': 400
|
||||
},
|
||||
{
|
||||
'init': [
|
||||
'get', '/message/get_range', {'channel': chan_d['id'], 'start-time': int(msg_chan_name), 'end-time': int(msg_chan_name)}
|
||||
],
|
||||
'auth': jwt, 'hope': 400
|
||||
},
|
||||
{
|
||||
'init': [
|
||||
'get', '/message/get_range', {'channel': chan_d['id'], 'end-time': int(msg_chan_name), 'start-time': int(msg_chan_name)}
|
||||
],
|
||||
'auth': jwt, 'hope': 400
|
||||
},
|
||||
# two tests that follow the rules
|
||||
{
|
||||
'init': [
|
||||
'get', '/message/from_id', {'start': 1, 'channel': 3}
|
||||
],
|
||||
'auth': jwt, 'hope': 200, 'body': True
|
||||
},
|
||||
{
|
||||
'init': [
|
||||
'get', '/message/from_id', {'start':1, 'channel':3, 'limit':2}
|
||||
],
|
||||
'auth': jwt, 'hope': 200, 'body': True
|
||||
},
|
||||
# tests that don't follow the api's rules
|
||||
{
|
||||
# channel doesn't exist so a 404 seems to be inorder
|
||||
'init': [
|
||||
'get', '/message/from_id', {'start': 1, 'channel':9}
|
||||
],
|
||||
'auth': jwt, 'hope': 404
|
||||
},
|
||||
{
|
||||
'init': [
|
||||
# good channel but id is tooo high
|
||||
'get', '/message/from_id', {'start': 5, 'channel':3}
|
||||
],
|
||||
'auth': jwt, 'hope': 404
|
||||
},
|
||||
]
|
||||
|
||||
for test in message_tests:
|
||||
method, path, opts = test['init']
|
||||
auth = test['auth']
|
||||
hope = test['hope']
|
||||
if 'body' in test:
|
||||
worker.request(method, path, auth, opts, hope, show_body=test['body'])
|
||||
else:
|
||||
worker.request(method, path, auth, opts, hope)
|
||||
|
||||
|
||||
|
||||
worker.logs()
|
||||
|
||||
if __name__ == '__main__':
|
||||
worker = Worker('http://localhost:8888', create_admin=True)
|
||||
run(worker)
|
||||
8
json-api/client-tests/pyvenv.cfg
Normal file
8
json-api/client-tests/pyvenv.cfg
Normal file
@@ -0,0 +1,8 @@
|
||||
home = /usr
|
||||
implementation = CPython
|
||||
version_info = 3.9.1.final.0
|
||||
virtualenv = 20.2.2
|
||||
include-system-site-packages = false
|
||||
base-prefix = /usr
|
||||
base-exec-prefix = /usr
|
||||
base-executable = /usr/bin/python3
|
||||
5
json-api/client-tests/requirements.txt
Normal file
5
json-api/client-tests/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
certifi==2020.12.5
|
||||
chardet==4.0.0
|
||||
idna==2.10
|
||||
requests==2.25.1
|
||||
urllib3==1.26.2
|
||||
1
json-api/client-tests/web/__init__.py
Normal file
1
json-api/client-tests/web/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import http
|
||||
126
json-api/client-tests/web/http.py
Normal file
126
json-api/client-tests/web/http.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import sys
|
||||
import requests
|
||||
import time
|
||||
import json
|
||||
|
||||
class RequestError(Exception):
|
||||
pass
|
||||
|
||||
class Response:
|
||||
'''
|
||||
Response is wrapper for reading + extracting information we care about
|
||||
Primarily created by Requests that get `make`'d.
|
||||
'''
|
||||
def __init__(self, method: str, url: str, body: str, code: int, expected: int, out=sys.stdout, color=True,
|
||||
truncate_long_body=True):
|
||||
|
||||
self.method = method
|
||||
self.url = url
|
||||
self.body = body #typically a string before parsing anything
|
||||
self.code = code #u16
|
||||
self.expected = expected #u16
|
||||
|
||||
self.out = out # file handle to write to normally sys.stdout
|
||||
self.color = color # bool telling if log should color anything (on by default)
|
||||
self.truncate_long_body = truncate_long_body
|
||||
|
||||
def _color(self, cc, string):
|
||||
nc = '\033[0m'
|
||||
return f'{cc}{string}{nc}'
|
||||
|
||||
def _color_failing(self, string):
|
||||
red = '\033[1;31m'
|
||||
return self._color(red, string)
|
||||
|
||||
|
||||
def _color_passing(self, string):
|
||||
green = '\033[1;32m'
|
||||
return self._color(green, string)
|
||||
|
||||
def __write_msg(self, s):
|
||||
# mega dumb wrapper to reduce visual noise i think
|
||||
if (len(s) == 1 or len(s) == 0) is False: print(s, file=self.out)
|
||||
|
||||
def _log_body(self):
|
||||
if self.truncate_long_body:
|
||||
if len(self.body) > 80:
|
||||
msg = self.body
|
||||
while len(msg) > 80:
|
||||
msg = msg[:len(msg)//2] + msg[len(msg)//2+1:]
|
||||
msg = '.....'.join([msg[:40], msg[44]])
|
||||
|
||||
self.__write_msg(msg)
|
||||
else:
|
||||
self.__write_msg(f'\t{self.body}')
|
||||
else:
|
||||
self.__write_msg(f'\t{self.body}')
|
||||
|
||||
def log(self):
|
||||
if self.code != self.expected:
|
||||
msg = f'Failed {self.method.upper()} {self.url}\t{self.code} expected {self.expected}'
|
||||
if self.color:
|
||||
msg = self._color_failing(msg)
|
||||
|
||||
self.__write_msg(msg)
|
||||
self._log_body()
|
||||
else:
|
||||
msg = f'Passing: {self.method} {self.url}'
|
||||
if self.color:
|
||||
msg = self._color_passing(msg)
|
||||
|
||||
self.__write_msg(msg)
|
||||
|
||||
def json(self):
|
||||
'''
|
||||
Force an interpretation of json from the body
|
||||
NOTE: this method is rather obtuse and a massive afterthough so its usage
|
||||
should be limited as much as possible
|
||||
'''
|
||||
try:
|
||||
return json.loads(self.body)
|
||||
except:
|
||||
return {}
|
||||
|
||||
def __str__(self):
|
||||
'''
|
||||
Returns: str(Response) -> `code => response.bdoy`
|
||||
'''
|
||||
return f'{self.code} => {self.body}'
|
||||
|
||||
class Request:
|
||||
def __init__(self, method: str, url: str, params: dict):
|
||||
assert(method in ['get', 'post', 'delete'])
|
||||
|
||||
self.method = method
|
||||
self.url = url
|
||||
self.params = params
|
||||
|
||||
def _make_request(self, method: str, hope: int):
|
||||
# Lower driver for actuall making the request we are looking for
|
||||
method = method.lower()
|
||||
|
||||
params = json.dumps(self.params)
|
||||
if method == 'get':
|
||||
resp = requests.get(self.url, data=params)
|
||||
return Response('get', self.url, resp.text, resp.status_code, hope)
|
||||
elif method == 'post':
|
||||
resp = requests.post(self.url, data=params)
|
||||
return Response('post', self.url, resp.text, resp.status_code, hope)
|
||||
elif method == 'delete':
|
||||
resp = requests.delete(self.url, data=params)
|
||||
return Response('delete', self.url, resp.text, resp.status_code, hope)
|
||||
|
||||
else:
|
||||
raise RequestError('Invalid method passed')
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
def make(self, hope: int) -> Response:
|
||||
'''
|
||||
@param hope: int -> status code we hope to get back
|
||||
@return Response -> Wrapper around server http response
|
||||
'''
|
||||
return self._make_request(self.method, hope)
|
||||
|
||||
|
||||
1576
json-api/db/Cargo.lock
generated
Normal file
1576
json-api/db/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
json-api/db/Cargo.toml
Normal file
17
json-api/db/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "db"
|
||||
version = "0.1.0"
|
||||
authors = ["shockrah <alejandros714@protonmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
mysql_async = "0.23.1"
|
||||
redis = { version = "0.18.0", features = ["tokio-comp"] }
|
||||
async-trait = "0.1.40"
|
||||
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0.117", features = [ "derive" ] }
|
||||
|
||||
chrono = "0.4.0"
|
||||
35
json-api/db/src/auth.rs
Normal file
35
json-api/db/src/auth.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use mysql_async::{params, Pool};
|
||||
use mysql_async::prelude::Queryable;
|
||||
use mysql_async::error::Error;
|
||||
use crate::UBigInt;
|
||||
|
||||
async fn update_jwt(p: &Pool, id: UBigInt, token: &str) -> Result<(), Error> {
|
||||
let conn = p.get_conn().await?;
|
||||
let _ = conn.drop_exec("UPDATE jwt SET token = :tk WHERE id = :id",
|
||||
params!{"tk" => token, "id" => id}).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_jwt(p: &Pool, id: UBigInt, token: &str) -> Result<(), Error> {
|
||||
let conn = p.get_conn().await?;
|
||||
let q = "INSERT INTO jwt (id, token) VALUES (:id, :tk)";
|
||||
return match conn.prep_exec(q, params!{"tk" => token, "id" => id}).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => update_jwt(p, id, token).await // attempt to refresh token
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn listed_jwt(p: &Pool, id: UBigInt, token_given: &str) -> Result<bool, Error> {
|
||||
// only checks if the given token is listed somewhere in the db
|
||||
let conn = p.get_conn().await?;
|
||||
let q = "SELECT token FROM jwt WHERE id = :id";
|
||||
// if id.token == return true
|
||||
let (_, db_t): (_, Option<String>) =
|
||||
conn.first_exec(q, params!{"id" => id}).await?;
|
||||
|
||||
|
||||
return match db_t {
|
||||
Some(token_db) => Ok(token_db == token_given), // probably pointless check but its not that expensive so its stays as a sanity check
|
||||
None => Ok(false)
|
||||
};
|
||||
}
|
||||
198
json-api/db/src/channels.rs
Normal file
198
json-api/db/src/channels.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use mysql_async::{params, Pool, Conn};
|
||||
use mysql_async::prelude::Queryable;
|
||||
use mysql_async::error::Error as SqlError;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{VarChar, UBigInt, Integer};
|
||||
use crate::common::FromDB;
|
||||
use crate::{sql_err, no_conn, Response};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Channel {
|
||||
pub id: UBigInt,
|
||||
pub name: VarChar,
|
||||
pub description: Option<VarChar>,
|
||||
pub kind: Integer
|
||||
}
|
||||
|
||||
pub const VOICE_CHANNEL: Integer = 1;
|
||||
pub const TEXT_CHANNEL: Integer = 2;
|
||||
|
||||
#[async_trait]
|
||||
impl FromDB<Channel, Integer> for Channel {
|
||||
// id name desc kind
|
||||
type Row = Option<(UBigInt, VarChar, Option<VarChar>, Integer)>;
|
||||
|
||||
async fn get(p: &Pool, id: UBigInt) -> Response<Channel> {
|
||||
//! Retrieves a Full single Channel row from the DB or fails in a
|
||||
//! fashion described by crate::Response<Channel>
|
||||
//! @param p -> SqlPool
|
||||
//! @param id -> UBigInt
|
||||
//! @return on_success -> Response::Row(Channel)
|
||||
//! @return on_fail -> Response::{Empty, Other<String>}
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let q = "SELECT id, name, description, kind FROM channels WHERE id = :id";
|
||||
let result: Result<(Conn, Self::Row), SqlError> =
|
||||
conn.first_exec(q, params!{"id" => id}).await;
|
||||
if let Ok((_, row)) = result {
|
||||
return match row {
|
||||
Some(row) => Response::Row(Channel {
|
||||
id: id,
|
||||
name: row.1,
|
||||
description: row.2,
|
||||
kind: row.3
|
||||
|
||||
}),
|
||||
None => Response::Empty
|
||||
}
|
||||
}
|
||||
return Response::Other(no_conn!("Invite::FromDB::get fetch failed"));
|
||||
}
|
||||
return Response::Other(no_conn!("Invite::FromDB::get"));
|
||||
}
|
||||
|
||||
async fn update(p: &Pool, row: Self) -> Response<Channel> {
|
||||
//! Updates a whole single based on a given Row Of Channel Type
|
||||
//! @param p -> SqlPool
|
||||
//! @param row -> Channel
|
||||
//! @return on_success -> Response::Success
|
||||
//! @return on_failure -> Response::Other
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let q = "UPDATE channels
|
||||
SET name = :name, description = :desc, kind = :kind
|
||||
WHERE id = :id";
|
||||
let result: Result<Conn, SqlError> =
|
||||
conn.drop_exec(q, params!{
|
||||
"id" => row.id,
|
||||
"name" => row.name,
|
||||
"desc" => row.description,
|
||||
"kind" => row.kind
|
||||
}).await;
|
||||
return match result {
|
||||
Ok(_) => Response::Success,
|
||||
Err(_) => Response::Other(sql_err!("Invite::FromDB::update Update failed"))
|
||||
}
|
||||
}
|
||||
return Response::Other(no_conn!("Invite::FromDB::get connection failed"));
|
||||
}
|
||||
|
||||
async fn delete(p: &Pool, id: UBigInt) -> Response<Channel> {
|
||||
//! Deletes channel given UBigInt as the row key
|
||||
//! @param p -> SqlPool
|
||||
//! @param id -> UBigInt
|
||||
//! @return on_success -> Response::Success
|
||||
//! @return on_failure -> Response::Other
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let q = "DELETE FROM channels WHERE id = :id";
|
||||
let result: Result<Conn, SqlError> =
|
||||
conn.drop_exec(q, params!{"id" => id}).await;
|
||||
return match result {
|
||||
Ok(_) => Response::Success,
|
||||
Err(sql) => Response::Other(sql_err!(sql))
|
||||
}
|
||||
}
|
||||
else {
|
||||
return Response::Other(no_conn!("Member::FromDB::delete"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn filter(p: &Pool, kind: Integer) -> Response<Channel> {
|
||||
//! @returns -> on success : Response::Set(Vec<Channel>)
|
||||
//! @returns -> on empty set : Response::Set(EmptyVector)
|
||||
//! @params -> on fail : Response::Other
|
||||
|
||||
// NOTE: used for mapping datasets to vectors
|
||||
let map_rows = |row| {
|
||||
let (id, name, desc, k): (UBigInt, VarChar, Option<VarChar>, Integer) =
|
||||
mysql_async::from_row(row);
|
||||
Channel {
|
||||
id: id,
|
||||
name: name,
|
||||
description: desc,
|
||||
kind: k
|
||||
}
|
||||
};
|
||||
return match (p.get_conn().await, kind) {
|
||||
// Filter for text/voice channels specifically
|
||||
(Ok(conn), VOICE_CHANNEL..=TEXT_CHANNEL) => { // @NOTE: voice channel and text_channel are literally 1 & 2 respectively
|
||||
let q = "SELECT id, name, description, kind FROM channels WHERE kind = :kind";
|
||||
let q_result = conn.prep_exec(q, params!{"kind" => kind}).await;
|
||||
if let Ok(res) = q_result {
|
||||
let mapping_result = res.map_and_drop(map_rows).await;
|
||||
return match mapping_result {
|
||||
Ok((_, channels)) => Response::Set(channels),
|
||||
Err(_) => Response::Other(sql_err!("db::Channels::filter @with params"))
|
||||
};
|
||||
}
|
||||
else {
|
||||
Response::Other(sql_err!(""))
|
||||
}
|
||||
},
|
||||
/*
|
||||
* Here we are attempting to get all the channels with no filters applied
|
||||
* This should fetch everything basically in our channels registry
|
||||
*/
|
||||
(Ok(conn), _) => {
|
||||
let q = "SELECT id, name, description, kind FROM channels";
|
||||
if let Ok(query) = conn.prep_exec(q, ()).await {
|
||||
let mapping_r = query.map_and_drop(map_rows).await;
|
||||
|
||||
return match mapping_r {
|
||||
Ok((_, channels)) => Response::Set(channels),
|
||||
Err(_) => Response::Other(sql_err!("db::Channels::filter @no params"))
|
||||
};
|
||||
}
|
||||
else {
|
||||
Response::Other(sql_err!("db::Channels::filter @no params @no initial query"))
|
||||
}
|
||||
},
|
||||
(Err(_), _) => {Response::Other(no_conn!("Channel::FromDB::filter"))}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub async fn add(p: &Pool, name: &str, desc: &str, kind: Integer) -> Response<Self> {
|
||||
//! @returns on success -> Response::Row<Channel>
|
||||
//! @returns on partial success -> Response::Empty
|
||||
//! @returns on failure -> Response::Other
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let q = "INSERT INTO channels (name, description, kind) VALUES (:n, :d, :k)";
|
||||
let insert_result = conn.drop_exec(q, params!{
|
||||
"n" => name,
|
||||
"d" => desc,
|
||||
"k" => kind
|
||||
}).await;
|
||||
if let Ok(conn) = insert_result {
|
||||
// This is only kosher because names are enforced as unique by sql
|
||||
let q = "SELECT id FROM channels WHERE name = :name";
|
||||
let fetch_result : Result<(Conn, Option<UBigInt>), SqlError> =
|
||||
conn.first_exec(q, params!{"name" => name}).await;
|
||||
|
||||
return match fetch_result {
|
||||
Ok((_, id_opt)) => {
|
||||
if let Some(id) = id_opt {
|
||||
Response::Row(Channel {
|
||||
id: id,
|
||||
name: name.into(),
|
||||
description: Some(desc.into()),
|
||||
kind: kind
|
||||
})
|
||||
}
|
||||
else { Response::Empty }
|
||||
},
|
||||
Err(_) => Response::Empty
|
||||
};
|
||||
}
|
||||
else {
|
||||
return Response::RestrictedInput(
|
||||
"Could not add channel, make sure the name is unique and not longer than 256 bytes".into());
|
||||
}
|
||||
}
|
||||
return Response::Other(no_conn!("db::channels::add"))
|
||||
}
|
||||
}
|
||||
|
||||
53
json-api/db/src/common.rs
Normal file
53
json-api/db/src/common.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use mysql_async::Pool;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::Response;
|
||||
use crate::UBigInt;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! no_conn {
|
||||
($spec:literal) => {
|
||||
format!("[ CON Error ] : {}", $spec)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! sql_err {
|
||||
($spec:literal) => {
|
||||
format!("[ SQL Error ] : {}", $spec)
|
||||
};
|
||||
|
||||
// Using this mostly to pull in sql err types from lib to outside world for logging
|
||||
($exp:expr) => {
|
||||
format!("[ SQL Error ] : {}", $exp)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! sql_err_log {
|
||||
($spec:expr) => {
|
||||
println!($spec);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* NOTE: pay attention to work on async in traits for rust
|
||||
* Each of one these funcs will implicitly do a single heap allocation which
|
||||
* for our case is fine though ideally we don't
|
||||
*
|
||||
* As soon as we finda way around that we should depecrate `#[async_trait`
|
||||
* for the performance
|
||||
* */
|
||||
|
||||
#[async_trait]
|
||||
pub trait FromDB<T, FilterType> {
|
||||
type Row;
|
||||
|
||||
async fn get(p: &Pool, id: UBigInt) -> Response<T>;
|
||||
|
||||
async fn update(p: &Pool, row: T) -> Response<T>;
|
||||
|
||||
async fn delete(p: &Pool, id: UBigInt) -> Response<T>;
|
||||
|
||||
async fn filter(p: &Pool, filter_val: FilterType) -> Response<T>;
|
||||
}
|
||||
116
json-api/db/src/invites.rs
Normal file
116
json-api/db/src/invites.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use mysql_async::{params, Pool, Conn};
|
||||
use mysql_async::prelude::Queryable;
|
||||
use mysql_async::error::Error as SqlError;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{UBigInt, BigInt};
|
||||
use crate::common::FromDB;
|
||||
use crate::{Response, no_conn};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Invite {
|
||||
pub id: BigInt,
|
||||
pub uses: Option<BigInt>,
|
||||
pub expires: bool
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FromDB<Invite, bool> for Invite {
|
||||
type Row = Option<(BigInt, Option<BigInt>, bool)>;
|
||||
|
||||
async fn get(p: &Pool, id: UBigInt) -> Response<Self> {
|
||||
// NOTE: cast is required for this as `id` is used as unix timestamp
|
||||
let id: BigInt = id as BigInt;
|
||||
|
||||
if id <= 0 {
|
||||
return Response::Empty;
|
||||
}
|
||||
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let q = "SELECT id, uses, expires FROM invites WHERE id = :id ";
|
||||
let result: Result<(Conn, Self::Row), SqlError> =
|
||||
conn.first_exec(q, params!{"id" => id}).await;
|
||||
|
||||
if let Ok((_, row)) = result {
|
||||
return match row {
|
||||
Some(row) => Response::Row(Self {
|
||||
id: id as BigInt,
|
||||
uses: row.1,
|
||||
expires: row.2
|
||||
}),
|
||||
None => Response::Empty
|
||||
}
|
||||
}
|
||||
}
|
||||
return Response::Empty;
|
||||
}
|
||||
|
||||
async fn update(p: &Pool, row: Self) -> Response<Self> {
|
||||
let q = r#"UPDATE invites
|
||||
SET uses = :uses, expires: :exp
|
||||
WHERE id = :id
|
||||
"#;
|
||||
// forcibly udpate even if theres nothing there
|
||||
// this way we avoid doing an extra network hit
|
||||
if row.id <= 0 {
|
||||
return Response::Empty;
|
||||
}
|
||||
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let result: Result<Conn, SqlError> =
|
||||
conn.drop_exec(q, params!{
|
||||
"id" => row.id,
|
||||
"uses" => row.uses,
|
||||
"exp" => row.expires
|
||||
}).await;
|
||||
|
||||
return match result {
|
||||
Ok(_) => Response::Success,
|
||||
Err(_) => Response::Other(format!("Could not update entry {}", row.id))
|
||||
}
|
||||
}
|
||||
return Response::Empty;
|
||||
}
|
||||
async fn delete(p: &Pool, id: UBigInt) -> Response<Self> {
|
||||
if id <= 0 { // really lame "assertion" that each method has to use for invites since they all internally use
|
||||
return Response::Empty;
|
||||
}
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let q = "DELETE FROM invites WHERE id = :id";
|
||||
let result: Result<Conn, SqlError> =
|
||||
conn.drop_exec(q, params!{"id" => id as BigInt}).await;
|
||||
return match result {
|
||||
Ok(_) => Response::Success,
|
||||
Err(_) => Response::Other(format!("Could not delete {}", id))
|
||||
}
|
||||
}
|
||||
return Response::Success;
|
||||
}
|
||||
async fn filter(p: &Pool, expirey_flag: bool) -> Response<Self> {
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let q = "SELECT id, uses, expires FROM invites WHERE expires = :exp";
|
||||
if let Ok(query) = conn.prep_exec(q, params!{"exp" => expirey_flag}).await {
|
||||
let mapping_r = query.map_and_drop(|row| {
|
||||
let (id, uses): (BigInt, Option<BigInt>) = mysql_async::from_row(row);
|
||||
Invite {
|
||||
id: id,
|
||||
uses: uses,
|
||||
expires: expirey_flag
|
||||
}
|
||||
}).await;
|
||||
return match mapping_r {
|
||||
Ok((_, invites)) => Response::Set(invites),
|
||||
Err(_) => Response::Empty
|
||||
}
|
||||
}
|
||||
else {
|
||||
return Response::Other(no_conn!("db::Invite::filter"));
|
||||
}
|
||||
}
|
||||
else {
|
||||
return Response::Other(no_conn!("db::Invites::filter"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
json-api/db/src/lib.rs
Normal file
45
json-api/db/src/lib.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
extern crate serde;
|
||||
pub mod member;
|
||||
pub mod common;
|
||||
pub mod invites;
|
||||
pub mod channels;
|
||||
pub mod messages;
|
||||
pub mod auth;
|
||||
|
||||
use std::vec::Vec;
|
||||
|
||||
pub type BigInt = i64;
|
||||
pub type UBigInt = u64;
|
||||
|
||||
pub type Integer = i32;
|
||||
pub type UInteger = u32;
|
||||
|
||||
pub type VarChar = String;
|
||||
|
||||
pub enum Response<T> {
|
||||
// A set of rows collected
|
||||
Set(Vec<T>),
|
||||
// Single row collected
|
||||
Row(T),
|
||||
// Nothing was found -> for select/update/delete's
|
||||
Empty,
|
||||
// Generic success for things like update/delete's
|
||||
Success,
|
||||
// Mask 500's with probable user generated errors like duplicate keys being added
|
||||
// or data that doesn't fit in column or something
|
||||
RestrictedInput(String),
|
||||
// Not sure what this will be used for but maybe its useful at some point
|
||||
Other(String)
|
||||
}
|
||||
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! fetch_row {
|
||||
($table:literal, $col:literal, $key:expr, $rtype:ty,$conn:expr) => {
|
||||
let lib_str = format!("SELECT * FROM {} WHERE {} = :{}", $table, $col, $col);
|
||||
let (_, $rtype) = $conn.first_exec!(lib_str, sql_params{
|
||||
$col => $key,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
211
json-api/db/src/member.rs
Normal file
211
json-api/db/src/member.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use mysql_async::{params, Pool, Conn};
|
||||
use mysql_async::prelude::Queryable;
|
||||
use mysql_async::error::Error as SqlError;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{Response, no_conn, sql_err};
|
||||
use crate::{UBigInt, BigInt, Integer, VarChar};
|
||||
|
||||
use crate::common::{FromDB};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Member {
|
||||
pub id: UBigInt,
|
||||
pub secret: VarChar,
|
||||
pub name: VarChar,
|
||||
pub joindate: BigInt,
|
||||
pub status: Integer,
|
||||
pub permissions: UBigInt,
|
||||
}
|
||||
|
||||
|
||||
pub const STATUS_ONLINE: Integer = 0;
|
||||
pub const STATUS_OFFLINE: Integer = 1;
|
||||
pub const STATUS_AWAY: Integer = 2;
|
||||
pub const STATUS_DO_NOT_DISTURB: Integer = 3;
|
||||
/*
|
||||
*
|
||||
* conn = getconn
|
||||
* result = conn.query
|
||||
* return response based on result
|
||||
*
|
||||
*/
|
||||
#[async_trait]
|
||||
impl FromDB<Member, Integer> for Member {
|
||||
type Row = Option<(VarChar, VarChar, BigInt, Integer, UBigInt)>;
|
||||
|
||||
async fn get(p: &Pool, id: UBigInt) -> Response<Self> {
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let q = "SELECT secret, name, joindate, status, permissions FROM members WHERE id = :id";
|
||||
let db_res : Result<(Conn, Self::Row), SqlError>
|
||||
= conn.first_exec(q, params!{"id" => id}).await;
|
||||
if let Ok((_, row)) = db_res {
|
||||
return match row {
|
||||
Some(row) => Response::Row(Self {
|
||||
id: id,
|
||||
secret: row.0,
|
||||
name: row.1,
|
||||
joindate: row.2,
|
||||
status: row.3,
|
||||
permissions: row.4
|
||||
}),
|
||||
None => Response::Empty
|
||||
}
|
||||
}
|
||||
return Response::Other(sql_err!("Fetch failed"));
|
||||
}
|
||||
return Response::Other(no_conn!("Member::FromDB::get"));
|
||||
}
|
||||
|
||||
async fn update(p: &Pool, row: Member) -> Response<Self> {
|
||||
let q = r#"UPDATE members
|
||||
SET status = :status, permissions = :perms, name = :name
|
||||
WHERE id = :id"#;
|
||||
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let query = conn.drop_exec(q, params!{
|
||||
"id" => row.id,
|
||||
"status" => row.status,
|
||||
"name" => row.name,
|
||||
"perms" => row.permissions
|
||||
}).await;
|
||||
|
||||
return match query {
|
||||
Ok(_) => Response::Success,
|
||||
Err(err) => Response::Other(sql_err!(err))
|
||||
}
|
||||
}
|
||||
|
||||
return Response::Other(no_conn!("db::Member::update"));
|
||||
}
|
||||
|
||||
async fn delete(p: &Pool, id: UBigInt) -> Response<Self> {
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let q = "DELETE from members WHERE id = :id";
|
||||
let db_result: Result<Conn, SqlError>
|
||||
= conn.drop_exec(q, params!{"id" => id}).await;
|
||||
match Member::get(p, id).await {
|
||||
Response::Row(_) => {
|
||||
if let Ok(conn) = db_result {
|
||||
return match conn.prep_exec("", params!{"id" => id}).await {
|
||||
Ok(_) => Response::Success,
|
||||
Err(_) => Response::Other(sql_err!("Member::FromDB::delete"))
|
||||
}
|
||||
}
|
||||
return Response::Success
|
||||
},
|
||||
Response::Empty => return Response::Empty,
|
||||
_ => return Response::Other(sql_err!("Member::FromDB::delete | another stupid get happened delid this"))
|
||||
}
|
||||
}
|
||||
|
||||
return Response::Empty;
|
||||
}
|
||||
|
||||
async fn filter(p: &Pool, status: Integer) -> Response<Self> {
|
||||
//! @params status
|
||||
return match (p.get_conn().await, status) {
|
||||
(Ok(conn), STATUS_ONLINE) | (Ok(conn), STATUS_OFFLINE) |
|
||||
(Ok(conn), STATUS_AWAY) | (Ok(conn), STATUS_DO_NOT_DISTURB) => {
|
||||
// TODO: Allow people to query somewhere in the middle of this set
|
||||
// i.e. we should be able to get user 100 -> 150 instead of just the
|
||||
// first 1000 people
|
||||
let q =
|
||||
"SELECT id, name, status, permissions FROM memebrs
|
||||
WHERE status = :stat LIMIT 1000"; // high limit for now
|
||||
if let Ok(query) = conn.prep_exec(q, params!{"stat" => status}).await {
|
||||
let mapping_r = query.map_and_drop(|row| {
|
||||
let (id, name, status, permissions): (UBigInt, VarChar, Integer, UBigInt) =
|
||||
mysql_async::from_row(row);
|
||||
|
||||
Member {
|
||||
id: id,
|
||||
secret: "".into(), // no show for obv reasons
|
||||
name: name,
|
||||
joindate: 0, // doesn't matter
|
||||
status: status,
|
||||
permissions: permissions
|
||||
}
|
||||
}).await;
|
||||
match mapping_r {
|
||||
Ok((_, members)) => Response::Set(members),
|
||||
Err(_) => Response::Other(sql_err!("db::Members::filter"))
|
||||
}
|
||||
}
|
||||
else {
|
||||
Response::Other(sql_err!("db::Members::filter"))
|
||||
}
|
||||
}
|
||||
_ => Response::Other(sql_err!("err"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Member {
|
||||
pub async fn add(p: &Pool, name: &str, secret: &str, perms: u64) -> Result<Response<Self>, SqlError> {
|
||||
//! @param {pool} p
|
||||
//! @param {&str} name of new user
|
||||
//! @param {&str} encrypted secret : userland auth module should have a function for this
|
||||
//! @param {u64} permissions mask
|
||||
|
||||
//! @returns : on_succes => Ok(Response<Member>)
|
||||
//! @returns : on_partial_succes => Ok(Response<Member>)
|
||||
//! @returns : on_failure => Err(SomeBS)
|
||||
use chrono::Utc;
|
||||
|
||||
let conn = p.get_conn().await?;
|
||||
|
||||
//name
|
||||
//perms
|
||||
//secret
|
||||
let now: BigInt = Utc::now().timestamp();
|
||||
|
||||
|
||||
let conn = conn.drop_exec(
|
||||
"INSERT INTO members(secret, name, joindate, status, permissions)
|
||||
VALUES(:secret, :name, :joindate, :status, :permissions)",
|
||||
mysql_async::params!{
|
||||
"secret" => secret.clone(),
|
||||
"name" => name.clone(),
|
||||
"joindate" => now,
|
||||
"status" => STATUS_ONLINE,
|
||||
"permissions" => perms
|
||||
}).await?;
|
||||
|
||||
let (_, opt_id): (Conn, Option<UBigInt>) = conn.first_exec(
|
||||
"SELECT id FROM members WHERE secret = :secret",
|
||||
params!{
|
||||
"secret" => secret.clone()
|
||||
}).await?;
|
||||
|
||||
if let Some(id) = opt_id {
|
||||
return Ok(Response::Row(Self {
|
||||
id: id,
|
||||
secret: secret.into(),
|
||||
name: name.into(),
|
||||
joindate: now,
|
||||
status: STATUS_ONLINE,
|
||||
permissions: perms
|
||||
}))
|
||||
}
|
||||
|
||||
return Ok(Response::Empty);
|
||||
}
|
||||
|
||||
pub async fn update_perms(p: &Pool, uid: UBigInt, permissions: UBigInt) -> Result<UBigInt, SqlError> {
|
||||
//! @return on_sucess Ok(NewPermisionsMask)
|
||||
//!
|
||||
let conn = p.get_conn().await?;
|
||||
conn.drop_exec(
|
||||
"UPDATE members SET permissions = :perms WHERE id = :id",
|
||||
params!{
|
||||
"id" => uid,
|
||||
"perms" => permissions
|
||||
}).await?;
|
||||
|
||||
Ok(permissions)
|
||||
}
|
||||
}
|
||||
245
json-api/db/src/messages.rs
Normal file
245
json-api/db/src/messages.rs
Normal file
@@ -0,0 +1,245 @@
|
||||
use mysql_async::{params, Pool, Conn};
|
||||
use mysql_async::prelude::Queryable;
|
||||
use mysql_async::error::Error as SqlError;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{Response, no_conn, sql_err};
|
||||
use crate::{UBigInt, BigInt, VarChar};
|
||||
|
||||
use crate::common::FromDB;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Serialize)]
|
||||
pub struct Message {
|
||||
pub id: UBigInt,
|
||||
pub time: BigInt,
|
||||
pub content: VarChar,
|
||||
pub author_id: UBigInt,
|
||||
pub channel_id: UBigInt
|
||||
}
|
||||
|
||||
const MAX_MESSAGES: u64 = 1000;
|
||||
#[async_trait]
|
||||
impl FromDB<Message, (BigInt, UBigInt)> for Message {
|
||||
type Row = Option<(UBigInt, BigInt, VarChar, UBigInt, UBigInt)>;
|
||||
|
||||
async fn get(p: &Pool, id: UBigInt) -> Response<Self> {
|
||||
//! Typically used as the backend to the .update(...) method to
|
||||
//! pick out a message to later edit
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let q = "SELECT id, time, content, author_id, channel_id WHERE id = :id";
|
||||
let result: Result<(Conn, Self::Row), SqlError> =
|
||||
conn.first_exec(q, params!{"id" => id}).await;
|
||||
if let Ok((_, row)) = result {
|
||||
return match row {
|
||||
Some(row) => Response::Row(Self {
|
||||
id,
|
||||
time: row.1,
|
||||
content: row.2,
|
||||
author_id: row.3,
|
||||
channel_id: row.4
|
||||
}),
|
||||
None => Response::Empty
|
||||
}
|
||||
}
|
||||
return Response::Other(sql_err!("Message::FromDB::get"));
|
||||
}
|
||||
return Response::Other(no_conn!("Message"));
|
||||
}
|
||||
|
||||
async fn update(p: &Pool, row: Self) -> Response<Self> {
|
||||
//! Primarily used by users to edit previous comments
|
||||
// NOTE: we only allow the changing of content in this since
|
||||
// no other column has good reason to be modified
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let q = "UPDATE messages
|
||||
SET content = :content
|
||||
WHERE id = :id";
|
||||
let result: Result<Conn, SqlError> =
|
||||
conn.drop_exec(q, params!{"id" => row.id, "content" => row.content}).await;
|
||||
return match result {
|
||||
Ok(_) => Response::Success,
|
||||
Err(_) => Response::Other(sql_err!("Message::FromDB::update"))
|
||||
}
|
||||
}
|
||||
return Response::Other(no_conn!("Message::FromDB::update"))
|
||||
}
|
||||
|
||||
async fn delete(p: &Pool, id: UBigInt) -> Response<Self> {
|
||||
//! Deletes a single message
|
||||
//! Typically used by normal users/bots to remove unwanted comments
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let q = "DELETE FROM messages WHERE id = :id";
|
||||
let result: Result<Conn, SqlError> =
|
||||
conn.drop_exec(q, params!{"id" => id}).await;
|
||||
return match result {
|
||||
Ok(_) => Response::Success,
|
||||
Err(_) => Response::Other(sql_err!("Message::FromDB::delete"))
|
||||
}
|
||||
}
|
||||
return Response::Other(no_conn!("Message::FromDB::update"))
|
||||
}
|
||||
|
||||
async fn filter(p: &Pool, (time, channel_id): (BigInt, UBigInt)) -> Response<Self> {
|
||||
//! FIlter happens via unix_timestamp and channel_id respectively
|
||||
if let Ok(conn) = p.get_conn().await {
|
||||
let q = "SELECT id, time, content, author_id";
|
||||
if let Ok(query)= conn.prep_exec(q, params!{"time" => time, "cid" => channel_id}).await {
|
||||
let mapping_r = query.map_and_drop(|row| {
|
||||
let (id, time, content, author_id): (UBigInt, BigInt, VarChar, UBigInt) =
|
||||
mysql_async::from_row(row);
|
||||
|
||||
Message {
|
||||
id,
|
||||
time,
|
||||
content,
|
||||
author_id,
|
||||
channel_id
|
||||
}
|
||||
}).await;
|
||||
|
||||
match mapping_r {
|
||||
Ok((_, messages)) => Response::Set(messages),
|
||||
Err(_) => Response::Other(sql_err!("db::Message::filter"))
|
||||
}
|
||||
}
|
||||
else {
|
||||
return Response::Empty;
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
return Response::Other(no_conn!("db::Message::filter"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub async fn send(p: &Pool, content: &str, cid: UBigInt, uid: UBigInt) -> Result<Response<Self>, SqlError> {
|
||||
//! @returns on_sucess -> empty
|
||||
//! @returns on_failure Err(SqlErr)
|
||||
|
||||
use chrono::Utc;
|
||||
let conn = p.get_conn().await?;
|
||||
let q = "INSERT INTO messages
|
||||
(time, content, author_id, channel_id)
|
||||
VALUES (:time, :content, :author, :channel)";
|
||||
let now = Utc::now().timestamp();
|
||||
|
||||
let res = conn.prep_exec(q, params!{
|
||||
"time" => now,
|
||||
"content" => content,
|
||||
"author" => uid,
|
||||
"channel" => cid
|
||||
}).await;
|
||||
if let Err(e) = res {
|
||||
return match e {
|
||||
SqlError::Server(err) => {
|
||||
if err.code == 1452 {
|
||||
return Ok(Response::RestrictedInput("Channel not found".into()))
|
||||
}
|
||||
else {
|
||||
Ok(Response::Other(sql_err!("db::messages::send")))
|
||||
}
|
||||
},
|
||||
_ => Ok(Response::Other(sql_err!("db::messages::send")))
|
||||
}
|
||||
|
||||
}
|
||||
// all good response
|
||||
else {
|
||||
return Ok(Response::Empty);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub async fn get_time_range(p: &Pool, channel_id: UBigInt, start: BigInt, end: BigInt, limit: Option<u64>) -> Result<Response<Self>, SqlError> {
|
||||
//! @returns on success : Set(Vec<Messages>)
|
||||
//! @returns on userfail: RestrictedInput(message)
|
||||
//! @returns on error : Err(SqlError)
|
||||
|
||||
if start >= end {
|
||||
Ok(Response::RestrictedInput("Invalid start/end parameters".into()))
|
||||
}
|
||||
else {
|
||||
let conn = p.get_conn().await?;
|
||||
let limit = if let Some(limit) = limit {
|
||||
match limit {
|
||||
1 ..= MAX_MESSAGES => limit,
|
||||
_ => MAX_MESSAGES
|
||||
}
|
||||
} else {
|
||||
MAX_MESSAGES
|
||||
};
|
||||
|
||||
let q = "SELECT id, time, content, author_id FROM messages
|
||||
WHERE channel_id = :channel AND time >= :start AND time < :end
|
||||
LIMIT :limit";
|
||||
|
||||
let select_result = conn.prep_exec(
|
||||
q, params!{
|
||||
"start" => start,
|
||||
"end" => end,
|
||||
"channel" => channel_id,
|
||||
"limit" => limit
|
||||
}).await?;
|
||||
|
||||
let(_conn, messages) = select_result.map_and_drop(|row| {
|
||||
type Tuple = (UBigInt, BigInt, String, UBigInt);
|
||||
let (id, time, content, author_id): Tuple = mysql_async::from_row(row);
|
||||
Self {
|
||||
id,
|
||||
time,
|
||||
content,
|
||||
author_id,
|
||||
channel_id
|
||||
}
|
||||
}).await?;
|
||||
|
||||
Ok(Response::Set(messages))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_from_id(p: &Pool, channel_id: UBigInt, start: UBigInt, limit: Option<UBigInt>) -> Result<Response<Self>, SqlError> {
|
||||
//! @returns on success : Set(Vec<Messages>)
|
||||
//! @returns on failure : Err(SqlError)
|
||||
let conn = p.get_conn().await?;
|
||||
let limit = if let Some(limit) = limit{
|
||||
match limit {
|
||||
1 ..= MAX_MESSAGES => limit,
|
||||
_ => MAX_MESSAGES
|
||||
}
|
||||
} else {
|
||||
MAX_MESSAGES // messages at a time
|
||||
};
|
||||
|
||||
let q = "SELECT id, time, content, author_id FROM messages WHERE
|
||||
channel_id = :channel AND id >= :start LIMIT :limit";
|
||||
|
||||
let params = params!{
|
||||
"channel" => channel_id,
|
||||
"start" => start,
|
||||
"limit" => limit
|
||||
};
|
||||
|
||||
let select_result = conn.prep_exec(q, params).await?;
|
||||
|
||||
let (_conn, messages) = select_result.map_and_drop(|row| {
|
||||
type Tuple = (UBigInt, BigInt, String, UBigInt);
|
||||
let (id, time, content, author_id): Tuple = mysql_async::from_row(row);
|
||||
Self {
|
||||
id,
|
||||
time,
|
||||
content,
|
||||
author_id,
|
||||
channel_id
|
||||
}
|
||||
}).await?;
|
||||
|
||||
Ok(Response::Set(messages))
|
||||
}
|
||||
}
|
||||
|
||||
49
json-api/endpoints.md
Normal file
49
json-api/endpoints.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Documenting the currently tested / working endpoints
|
||||
|
||||
Mostly a dev reference for now since it's a lot more terse.
|
||||
|
||||
## Parameters
|
||||
|
||||
All parameters are to be passed in via json, other parameters are ignored
|
||||
|
||||
## Auth
|
||||
|
||||
Basically every endpoint except for `/join` and `/meta` require the folowing auth
|
||||
data at a bare minimum.
|
||||
|
||||
* id: u64 -> User id [ Public to the instance ]
|
||||
|
||||
* jwt: String
|
||||
|
||||
To get a JWT you must `POST /login` with the following:
|
||||
|
||||
* id: u64 -> User id [ Public to the instance ]
|
||||
|
||||
* secret: String -> Server generated password
|
||||
|
||||
## Chat api
|
||||
|
||||
`/channels/list` NoPermsRequired
|
||||
|
||||
No parameters required
|
||||
|
||||
`/channels/create` RequiredPerm[CREATE_CHANNEL=64]
|
||||
|
||||
* @Required name: `String`
|
||||
|
||||
* @Required kind: `Signed 32-bit Integer`
|
||||
|
||||
* @Optional description: `String`
|
||||
|
||||
For voice channel `1`. For text channel `2`. All other options result in a HTTP 400.
|
||||
|
||||
|
||||
`/channels/delete` RequiredPerm[DELETE_CHANNEL=128]
|
||||
|
||||
* @Required channel_id: `Unsigned 64-bit Integer`
|
||||
|
||||
`/message/send` RequiredPerm[SEND_MESSAGES=2]
|
||||
|
||||
* @Required channel `Unsigned 64-bit Integer`
|
||||
|
||||
* @Required content `String`
|
||||
2
json-api/hmac.secret
Normal file
2
json-api/hmac.secret
Normal file
@@ -0,0 +1,2 @@
|
||||
bïò<EFBFBD>Cˆ'BIŸ‘¬%©b»jˆ]’<>Ž¥µH>ŒŒ$ °b¾ æúϦwÙY攘#ÂýÐ
|
||||
4O¶>'ŸÉ“
|
||||
0
json-api/migrations/.gitkeep
Normal file
0
json-api/migrations/.gitkeep
Normal file
2
json-api/migrations/2020-02-04-083657_invites/down.sql
Normal file
2
json-api/migrations/2020-02-04-083657_invites/down.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE `invites`;
|
||||
12
json-api/migrations/2020-02-04-083657_invites/up.sql
Normal file
12
json-api/migrations/2020-02-04-083657_invites/up.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- @id : id of the invite which is also its kill date
|
||||
|
||||
-- @uses : can be null which means it doesn't have a use limit
|
||||
|
||||
-- @expires: boolean that tells wether the key expires or not
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `invites` (
|
||||
`id` BIGINT UNIQUE NOT NULL,
|
||||
`uses` BIGINT,
|
||||
`expires` BOOLEAN NOT NULL,
|
||||
PRIMARY KEY( `id` )
|
||||
);
|
||||
2
json-api/migrations/2020-03-11-005217_channels/down.sql
Normal file
2
json-api/migrations/2020-03-11-005217_channels/down.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE `channels`;
|
||||
8
json-api/migrations/2020-03-11-005217_channels/up.sql
Normal file
8
json-api/migrations/2020-03-11-005217_channels/up.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- TODO: somehow make the name colum unique
|
||||
CREATE TABLE IF NOT EXISTS `channels` (
|
||||
`id` BIGINT UNSIGNED NOT NULL auto_increment,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`description` VARCHAR(1024),
|
||||
`kind` INTEGER NOT NULL,
|
||||
PRIMARY KEY(`id`), UNIQUE KEY(`name`)
|
||||
);
|
||||
1
json-api/migrations/2020-07-05-215114_members/down.sql
Normal file
1
json-api/migrations/2020-07-05-215114_members/down.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE `members`;
|
||||
11
json-api/migrations/2020-07-05-215114_members/up.sql
Normal file
11
json-api/migrations/2020-07-05-215114_members/up.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- TODO: add rate limiter in some form
|
||||
-- PERMISSIONS start at 0 and full perms => all F's
|
||||
CREATE TABLE IF NOT EXISTS `members`(
|
||||
`id` BIGINT UNSIGNED NOT NULL auto_increment,
|
||||
`secret` varchar(256) NOT NULL,
|
||||
`name` varchar(256) NOT NULL,
|
||||
`joindate` bigint NOT NULL,
|
||||
`status` integer NOT NULL,
|
||||
`permissions` bigint UNSIGNED NOT NULL,
|
||||
PRIMARY KEY( `id` , `secret` )
|
||||
);
|
||||
1
json-api/migrations/2020-07-06-022319_messages/down.sql
Normal file
1
json-api/migrations/2020-07-06-022319_messages/down.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE `messages`;
|
||||
11
json-api/migrations/2020-07-06-022319_messages/up.sql
Normal file
11
json-api/migrations/2020-07-06-022319_messages/up.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Time stamp is _not_ in ms
|
||||
CREATE TABLE IF NOT EXISTS `messages`(
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`time` BIGINT NOT NULL,
|
||||
`content` VARCHAR(2048) NOT NULL,
|
||||
`author_id` BIGINT UNSIGNED NOT NULL,
|
||||
`channel_id` BIGINT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
FOREIGN KEY (`author_id`) REFERENCES members(`id`),
|
||||
FOREIGN KEY (`channel_id`) REFERENCES channels(`id`) ON DELETE CASCADE
|
||||
);
|
||||
2
json-api/migrations/2020-12-29-030934_jwt/down.sql
Normal file
2
json-api/migrations/2020-12-29-030934_jwt/down.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE `jwt`;
|
||||
5
json-api/migrations/2020-12-29-030934_jwt/up.sql
Normal file
5
json-api/migrations/2020-12-29-030934_jwt/up.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS `jwt`(
|
||||
`id` BIGINT UNSIGNED NOT NULL,
|
||||
`token` VARCHAR(256) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
);
|
||||
3
json-api/scripts/generate-hmac.sh
Normal file
3
json-api/scripts/generate-hmac.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
cat /dev/urandom | fold -bw 16 | base64 | head -1
|
||||
8
json-api/scripts/setup-dev-user.sh
Normal file
8
json-api/scripts/setup-dev-user.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Running the
|
||||
mysql -u root -p \
|
||||
"create user 'freechat_dev'@'localhost' identified by 'password';"
|
||||
|
||||
mysql -u root -p \
|
||||
"GRANT ALL PRIVILEGES ON freechat.* TO 'freechat_dev'@'localhost';"
|
||||
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