simple bot uploads

This commit is contained in:
Ilion Beyst 2021-12-19 00:16:46 +01:00
parent 2dbb085008
commit 52242b03f1
15 changed files with 328 additions and 32 deletions

View file

@ -7,7 +7,7 @@ edition = "2021"
[dependencies]
rocket = { version= "0.5.0-rc.1", features = ["json"] }
diesel = { version = "1.4.4", features = ["postgres", "r2d2"] }
diesel = { version = "1.4.4", features = ["postgres", "r2d2", "chrono"] }
dotenv = "0.15.0"
rust-argon2 = "0.8"
rand = "0.8.4"
@ -16,6 +16,7 @@ serde_bytes = "0.11"
chrono = { version = "0.4", features = ["serde"] }
serde_json = "1.0"
base64 = "0.13.0"
zip = "0.5"
[dependencies.rocket_sync_db_pools]

View file

@ -0,0 +1,3 @@
DROP TABLE code_bundles;
DROP INDEX bots_index;
DROP TABLE bots;

View file

@ -0,0 +1,14 @@
CREATE TABLE bots (
id serial PRIMARY KEY,
owner_id integer REFERENCES users(id) NOT NULL,
name text NOT NULL
);
CREATE UNIQUE INDEX bots_index ON bots(owner_id, name);
CREATE TABLE code_bundles (
id serial PRIMARY KEY,
bot_id integer REFERENCES bots(id) NOT NULL,
path text NOT NULL,
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
);

54
backend/src/db/bots.rs Normal file
View file

@ -0,0 +1,54 @@
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use crate::schema::{bots, code_bundles};
use crate::DbConn;
use chrono;
#[derive(Insertable)]
#[table_name = "bots"]
pub struct NewBot<'a> {
pub owner_id: i32,
pub name: &'a str,
}
#[derive(Queryable, Debug, PartialEq, Serialize, Deserialize)]
pub struct Bot {
pub id: i32,
pub owner_id: i32,
pub name: String,
}
pub fn create_bot(new_bot: &NewBot, conn: &PgConnection) -> QueryResult<Bot> {
diesel::insert_into(bots::table)
.values(new_bot)
.get_result(conn)
}
pub fn find_bot(id: i32, conn: &PgConnection) -> QueryResult<Bot> {
bots::table.find(id).first(conn)
}
#[derive(Insertable)]
#[table_name = "code_bundles"]
pub struct NewCodeBundle<'a> {
pub bot_id: i32,
pub path: &'a str,
}
#[derive(Queryable, Serialize, Deserialize, Debug)]
pub struct CodeBundle {
pub id: i32,
pub bot_id: i32,
pub path: String,
pub created_at: chrono::NaiveDateTime,
}
pub fn create_code_bundle(
new_code_bundle: &NewCodeBundle,
conn: &PgConnection,
) -> QueryResult<CodeBundle> {
diesel::insert_into(code_bundles::table)
.values(new_code_bundle)
.get_result(conn)
}

View file

@ -1,2 +1,3 @@
pub mod bots;
pub mod sessions;
pub mod users;

View file

@ -22,7 +22,7 @@ pub struct Session {
pub fn create_session(user: &User, conn: &PgConnection) -> Session {
let new_session = NewSession {
token: gen_session_token(),
user_id: user.user_id,
user_id: user.id,
};
let session = insert_into(sessions::table)
.values(&new_session)

View file

@ -20,7 +20,7 @@ pub struct NewUser<'a> {
#[derive(Queryable, Debug)]
pub struct User {
pub user_id: i32,
pub id: i32,
pub username: String,
pub password_salt: Vec<u8>,
pub password_hash: Vec<u8>,

View file

@ -8,9 +8,9 @@ extern crate rocket;
#[macro_use]
extern crate diesel;
mod db;
mod routes;
mod schema;
pub mod db;
pub mod routes;
pub mod schema;
#[database("postgresql_database")]
pub struct DbConn(diesel::PgConnection);
@ -29,6 +29,9 @@ pub fn rocket() -> Rocket<Build> {
routes::users::register,
routes::users::login,
routes::users::current_user,
routes::bots::create_bot,
routes::bots::get_bot,
routes::bots::upload_bot_code,
],
)
.attach(DbConn::fairing())

View file

@ -0,0 +1,96 @@
use rand::Rng;
use rocket::data::ToByteUnit;
use rocket::fs::TempFile;
use rocket::Data;
use rocket::{response::status, serde::json::Json};
use serde::{Deserialize, Serialize};
use std::io::Cursor;
use std::path::Path;
use crate::DbConn;
use crate::db::bots::{self, CodeBundle};
use crate::db::users::User;
use bots::Bot;
#[derive(Serialize, Deserialize, Debug)]
pub struct BotParams {
name: String,
}
// TODO: handle errors
#[post("/bots", data = "<params>")]
pub async fn create_bot(
db_conn: DbConn,
user: User,
params: Json<BotParams>,
) -> status::Created<Json<Bot>> {
db_conn
.run(move |conn| {
let bot_params = bots::NewBot {
owner_id: user.id,
name: &params.name,
};
let bot = bots::create_bot(&bot_params, conn).unwrap();
let bot_url = uri!(get_bot(bot.id)).to_string();
status::Created::new(bot_url).body(Json(bot))
})
.await
}
// TODO: handle errors
#[get("/bots/<bot_id>")]
pub async fn get_bot(db_conn: DbConn, bot_id: i32) -> Json<Bot> {
db_conn
.run(move |conn| {
let bot = bots::find_bot(bot_id, conn).unwrap();
Json(bot)
})
.await
}
// TODO: proper error handling
#[post("/bots/<bot_id>/upload", data = "<data>")]
pub async fn upload_bot_code(
db_conn: DbConn,
user: User,
bot_id: i32,
data: Data<'_>,
) -> status::Created<Json<CodeBundle>> {
// TODO: put in config somewhere
let data_path = "./data/bots";
let bot = db_conn
.run(move |conn| bots::find_bot(bot_id, conn))
.await
.expect("Bot not found");
assert_eq!(user.id, bot.owner_id);
// generate a random filename
let token: [u8; 16] = rand::thread_rng().gen();
let name = base64::encode(&token);
let path = Path::new(data_path).join(name);
let capped_buf = data.open(10usize.megabytes()).into_bytes().await.unwrap();
assert!(capped_buf.is_complete());
let buf = capped_buf.into_inner();
zip::ZipArchive::new(Cursor::new(buf))
.unwrap()
.extract(&path)
.unwrap();
let code_bundle = db_conn
.run(move |conn| {
let bundle = bots::NewCodeBundle {
bot_id: bot.id,
path: path.to_str().unwrap(),
};
bots::create_code_bundle(&bundle, conn).expect("Failed to create code bundle")
})
.await;
// TODO: proper location
status::Created::new("").body(Json(code_bundle))
}

View file

@ -1 +1,2 @@
pub mod bots;
pub mod users;

View file

@ -55,7 +55,7 @@ pub struct UserData {
impl From<User> for UserData {
fn from(user: User) -> Self {
UserData {
user_id: user.user_id,
user_id: user.id,
username: user.username,
}
}

View file

@ -1,3 +1,20 @@
table! {
bots (id) {
id -> Int4,
owner_id -> Int4,
name -> Text,
}
}
table! {
code_bundles (id) {
id -> Int4,
bot_id -> Int4,
path -> Text,
created_at -> Timestamp,
}
}
table! {
sessions (id) {
id -> Int4,
@ -15,6 +32,8 @@ table! {
}
}
joinable!(bots -> users (owner_id));
joinable!(code_bundles -> bots (bot_id));
joinable!(sessions -> users (user_id));
allow_tables_to_appear_in_same_query!(sessions, users,);
allow_tables_to_appear_in_same_query!(bots, code_bundles, sessions, users,);

98
backend/tests/bots.rs Normal file
View file

@ -0,0 +1,98 @@
#![feature(async_closure)]
extern crate mozaic4_backend;
extern crate zip;
use rocket::http::{ContentType, Status};
mod util;
use mozaic4_backend::db::{bots, sessions, users};
use mozaic4_backend::DbConn;
use sessions::Session;
use users::{Credentials, User};
use util::{run_test, BearerAuth};
async fn user_with_session(conn: &DbConn) -> (User, Session) {
conn.run(|conn| {
let credentials = Credentials {
username: "piepkonijn",
password: "geheim123",
};
let user = users::create_user(&credentials, conn).unwrap();
let session = sessions::create_session(&user, conn);
(user, session)
})
.await
}
#[rocket::async_test]
async fn test_bot_create() {
run_test(async move |client, conn| {
let (user, session) = user_with_session(&conn).await;
let response = client
.post("/bots")
.header(BearerAuth::new(session.token.clone()))
.header(ContentType::JSON)
.body(
r#"{
"name": "testbot"
}"#,
)
.dispatch()
.await;
assert_eq!(response.status(), Status::Created);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let resp_text = response.into_string().await.unwrap();
let json: serde_json::Value = serde_json::from_str(&resp_text).unwrap();
assert_eq!(json["name"], "testbot");
assert_eq!(json["owner_id"], user.id);
})
.await
}
// create an example zipfile for bot upload
fn create_zip() -> std::io::Result<Vec<u8>> {
use std::io::Write;
use zip::write::FileOptions;
let cursor = std::io::Cursor::new(Vec::new());
let mut zip = zip::ZipWriter::new(cursor);
zip.start_file("test.txt", FileOptions::default())?;
zip.write_all(b"sup brudi")?;
let buf = zip.finish()?;
Ok(buf.into_inner())
}
#[rocket::async_test]
async fn test_bot_upload() {
run_test(async move |client, conn| {
let (user, session) = user_with_session(&conn).await;
let owner_id = user.id;
let bot = conn
.run(move |conn| {
let new_bot = bots::NewBot {
name: "testbot",
owner_id: owner_id,
};
bots::create_bot(&new_bot, conn).unwrap()
})
.await;
let zip_file = create_zip().unwrap();
let response = client
.post(format!("/bots/{}/upload", bot.id))
.header(BearerAuth::new(session.token.clone()))
.header(ContentType::JSON)
.body(zip_file)
.dispatch()
.await;
assert_eq!(response.status(), Status::Created);
})
.await
}

View file

@ -1,27 +1,11 @@
#![feature(async_closure)]
extern crate mozaic4_backend;
use rocket::http::{ContentType, Header, Status};
use rocket::http::{ContentType, Status};
mod util;
use util::run_test;
pub struct BearerAuth {
token: String,
}
impl BearerAuth {
pub fn new(token: String) -> Self {
Self { token }
}
}
impl<'a> Into<Header<'a>> for BearerAuth {
fn into(self) -> Header<'a> {
Header::new("Authorization", format!("Bearer {}", self.token))
}
}
#[rocket::async_test]
async fn test_registration() {
run_test(async move |client, _conn| {
@ -47,7 +31,7 @@ async fn test_registration() {
let response = client
.get("/users/me")
.header(BearerAuth::new(token))
.header(util::BearerAuth::new(token))
.dispatch()
.await;
@ -56,7 +40,8 @@ async fn test_registration() {
let resp = response.into_string().await.unwrap();
let json: serde_json::Value = serde_json::from_str(&resp).unwrap();
assert_eq!(json["username"], "piepkonijn");
}).await
})
.await
}
#[rocket::async_test]
@ -71,5 +56,6 @@ async fn test_reject_invalid_credentials() {
assert_eq!(response.status(), Status::Forbidden);
// assert_eq!(response.content_type(), Some(ContentType::JSON));
}).await
})
.await
}

View file

@ -2,7 +2,7 @@ use std::future::Future;
use diesel::RunQueryDsl;
use mozaic4_backend::DbConn;
use rocket::local::asynchronous::Client;
use rocket::{http::Header, local::asynchronous::Client};
// We use a lock to synchronize between tests so DB operations don't collide.
// For now. In the future, we'll have a nice way to run each test in a DB
@ -11,7 +11,11 @@ static DB_LOCK: parking_lot::Mutex<()> = parking_lot::const_mutex(());
async fn reset_db(db: &DbConn) {
db.run(|conn| {
diesel::sql_query("TRUNCATE TABLE users, sessions")
diesel::sql_query(
r#"
TRUNCATE TABLE users, sessions,
bots, code_bundles"#,
)
.execute(conn)
.expect("drop all tables");
})
@ -37,3 +41,19 @@ where
test_closure(client, db).await;
}
pub struct BearerAuth {
token: String,
}
impl BearerAuth {
pub fn new(token: String) -> Self {
Self { token }
}
}
impl<'a> Into<Header<'a>> for BearerAuth {
fn into(self) -> Header<'a> {
Header::new("Authorization", format!("Bearer {}", self.token))
}
}