diff options
Diffstat (limited to 'server')
9 files changed, 486 insertions, 0 deletions
diff --git a/server/.sqlx/query-322c1c671af1dc5daf2303180e5ebb59fc78c9a71a34ec3f4309c2b495b1eeee.json b/server/.sqlx/query-322c1c671af1dc5daf2303180e5ebb59fc78c9a71a34ec3f4309c2b495b1eeee.json new file mode 100644 index 0000000..9ec46e5 --- /dev/null +++ b/server/.sqlx/query-322c1c671af1dc5daf2303180e5ebb59fc78c9a71a34ec3f4309c2b495b1eeee.json @@ -0,0 +1,58 @@ +{ + "db_name": "MySQL", + "query": "SELECT id,kind,data,comment FROM user_keys WHERE id=? AND user=?", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT", + "char_set": 63, + "max_size": 20 + } + }, + { + "ordinal": 1, + "name": "kind", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 512 + } + }, + { + "ordinal": 2, + "name": "data", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 32768 + } + }, + { + "ordinal": 3, + "name": "comment", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 4096 + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "322c1c671af1dc5daf2303180e5ebb59fc78c9a71a34ec3f4309c2b495b1eeee" +} diff --git a/server/.sqlx/query-7dff27857ca04ff487c71c6cc575b5888bfd6b7e3aa60fb50d58fb2e46231e79.json b/server/.sqlx/query-7dff27857ca04ff487c71c6cc575b5888bfd6b7e3aa60fb50d58fb2e46231e79.json new file mode 100644 index 0000000..5b52cc0 --- /dev/null +++ b/server/.sqlx/query-7dff27857ca04ff487c71c6cc575b5888bfd6b7e3aa60fb50d58fb2e46231e79.json @@ -0,0 +1,25 @@ +{ + "db_name": "MySQL", + "query": "SELECT COUNT(id) AS count FROM user_keys WHERE user=?", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | BINARY", + "char_set": 63, + "max_size": 21 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "7dff27857ca04ff487c71c6cc575b5888bfd6b7e3aa60fb50d58fb2e46231e79" +} diff --git a/server/.sqlx/query-947c8d9f63c791b1e56eb85cd4423b22f84fd28b8a07f21fbcee0d907873c591.json b/server/.sqlx/query-947c8d9f63c791b1e56eb85cd4423b22f84fd28b8a07f21fbcee0d907873c591.json new file mode 100644 index 0000000..4ce7b53 --- /dev/null +++ b/server/.sqlx/query-947c8d9f63c791b1e56eb85cd4423b22f84fd28b8a07f21fbcee0d907873c591.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "DELETE FROM user_keys WHERE id=? AND user=?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "947c8d9f63c791b1e56eb85cd4423b22f84fd28b8a07f21fbcee0d907873c591" +} diff --git a/server/.sqlx/query-a384335c775a26f6780e8b843507c69b3d34a4ef3f30ce5fefc0d3bc2e284faa.json b/server/.sqlx/query-a384335c775a26f6780e8b843507c69b3d34a4ef3f30ce5fefc0d3bc2e284faa.json new file mode 100644 index 0000000..9d50b8a --- /dev/null +++ b/server/.sqlx/query-a384335c775a26f6780e8b843507c69b3d34a4ef3f30ce5fefc0d3bc2e284faa.json @@ -0,0 +1,58 @@ +{ + "db_name": "MySQL", + "query": "SELECT id,kind,data,comment FROM user_keys WHERE user=? ORDER BY id LIMIT ? OFFSET ?", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT", + "char_set": 63, + "max_size": 20 + } + }, + { + "ordinal": 1, + "name": "kind", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 512 + } + }, + { + "ordinal": 2, + "name": "data", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 32768 + } + }, + { + "ordinal": 3, + "name": "comment", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 4096 + } + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "a384335c775a26f6780e8b843507c69b3d34a4ef3f30ce5fefc0d3bc2e284faa" +} diff --git a/server/.sqlx/query-ec7cb8523695d2bf73721631e4732e97e6119d519263f8b188550fc2bfe707f3.json b/server/.sqlx/query-ec7cb8523695d2bf73721631e4732e97e6119d519263f8b188550fc2bfe707f3.json new file mode 100644 index 0000000..b1aa378 --- /dev/null +++ b/server/.sqlx/query-ec7cb8523695d2bf73721631e4732e97e6119d519263f8b188550fc2bfe707f3.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "INSERT INTO user_keys (user, kind, data, comment) VALUES (?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "ec7cb8523695d2bf73721631e4732e97e6119d519263f8b188550fc2bfe707f3" +} diff --git a/server/migrations/1_initial_eyeballs.sql b/server/migrations/1_initial_eyeballs.sql index 3f31781..728c329 100644 --- a/server/migrations/1_initial_eyeballs.sql +++ b/server/migrations/1_initial_eyeballs.sql @@ -69,3 +69,16 @@ CREATE TABLE IF NOT EXISTS review_users ( ON DELETE CASCADE ON UPDATE RESTRICT ); + +CREATE TABLE IF NOT EXISTS user_keys ( + id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + user VARCHAR(128) NOT NULL, + kind VARCHAR(128) NOT NULL, + data VARCHAR(8192) NOT NULL, + comment VARCHAR(1024) NOT NULL, + + CONSTRAINT `fk_user_keys_user` + FOREIGN KEY (user) REFERENCES users (id) + ON DELETE CASCADE + ON UPDATE RESTRICT +); diff --git a/server/src/api_model.rs b/server/src/api_model.rs index 2dc20f1..f062935 100644 --- a/server/src/api_model.rs +++ b/server/src/api_model.rs @@ -201,3 +201,38 @@ pub struct StatusResponse { )] pub error: Option<&'static str>, } + +#[derive(Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct UserKey { + #[schema(example = 1u64)] + pub id: u64, + #[schema(example = "ssh-rsa")] + pub kind: String, + #[schema(example = "AAAAfoobar==")] + pub data: String, + #[schema(example = "user@host 1970-01-01")] + pub comment: String, +} + +#[derive(Deserialize, Serialize, ToSchema)] +pub struct UserKeyData<'r> { + #[schema(example = "ssh-rsa")] + pub kind: &'r str, + #[schema(example = "AAAAfoobar==")] + pub data: &'r str, + #[schema(example = "user@host 1970-01-01")] + pub comment: Option<&'r str>, +} + +#[derive(Deserialize, Serialize, ToSchema)] +pub struct UserKeys { + #[schema(example = 0u32)] + pub offset: u32, + #[schema(example = 10u32)] + pub limit: u32, + #[schema(example = 2u32)] + pub total_count: u32, + #[schema(example = false)] + pub more: bool, + pub keys: Vec<UserKey>, +} diff --git a/server/src/main.rs b/server/src/main.rs index d0547c1..298a418 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -44,6 +44,10 @@ struct Db(sqlx::MySqlPool); reviews, review, users, + user_key_add, + user_key_get, + user_key_del, + user_keys, ), modifiers(&AuthApiAddon), )] @@ -662,6 +666,162 @@ async fn users( }) } +#[utoipa::path( + responses( + (status = 200, description = "Key added to current user", body = api_model::UserKey), + (status = 400, description = "Key too large"), + ), + security( + ("session" = []), + ), +)] +#[post("/user/keys/add", data = "<data>")] +async fn user_key_add( + mut db: Connection<Db>, + session: auth::Session, + data: Json<api_model::UserKeyData<'_>>, +) -> Result<Json<api_model::UserKey>, Custom<&'static str>> { + if data.data.len() > 8192 { + return Err(Custom(Status::BadRequest, "Key is too large")); + } + + let comment = data.comment.unwrap_or(""); + let result = sqlx::query!( + "INSERT INTO user_keys (user, kind, data, comment) VALUES (?, ?, ?, ?)", + session.user_id, + data.kind, + data.data, + comment, + ) + .execute(&mut **db) + .await + .unwrap(); + + Ok(Json(api_model::UserKey { + id: result.last_insert_id(), + kind: data.kind.to_string(), + data: data.data.to_string(), + comment: comment.to_string(), + })) +} + +#[utoipa::path( + responses( + (status = 200, description = "User key", body = api_model::UserKey), + (status = 404, description = "No such key"), + ), + security( + ("session" = []), + ), +)] +#[get("/user/keys/<id>")] +async fn user_key_get( + mut db: Connection<Db>, + session: auth::Session, + id: u64, +) -> Result<Json<api_model::UserKey>, NotFound<&'static str>> { + let user_key = sqlx::query!( + "SELECT id,kind,data,comment FROM user_keys WHERE id=? AND user=?", + id, + session.user_id, + ) + .fetch_one(&mut **db) + .map_ok(|r| api_model::UserKey { + id: r.id, + kind: r.kind, + data: r.data, + comment: r.comment, + }) + .map_err(|_| NotFound("No such user key")) + .await?; + + Ok(Json(user_key)) +} + +#[utoipa::path( + responses( + (status = 200, description = "Key removed from current user"), + (status = 404, description = "No such key for current user"), + ), + security( + ("session" = []), + ), +)] +#[delete("/user/keys/<id>")] +async fn user_key_del( + mut db: Connection<Db>, + session: auth::Session, + id: u64, +) -> Result<&'static str, Custom<&'static str>> { + let result = sqlx::query!( + "DELETE FROM user_keys WHERE id=? AND user=?", + id, + session.user_id, + ) + .execute(&mut **db) + .await + .unwrap(); + if result.rows_affected() == 0 { + return Err(Custom(Status::NotFound, "No such key for current user")); + } + + Ok("") +} + +#[utoipa::path( + responses( + (status = 200, description = "Get all keys for user", body = api_model::UserKeys), + ), + security( + ("session" = []), + ), +)] +#[get("/user/keys?<limit>&<offset>")] +async fn user_keys( + mut db: Connection<Db>, + session: auth::Session, + limit: Option<u32>, + offset: Option<u32>, +) -> Json<api_model::UserKeys> { + let uw_offset = offset.unwrap_or(0); + let uw_limit = limit.unwrap_or(10); + let entries = sqlx::query!( + "SELECT id,kind,data,comment FROM user_keys WHERE user=? ORDER BY id LIMIT ? OFFSET ?", + session.user_id, + uw_limit, + uw_offset + ) + .fetch(&mut **db) + .map_ok(|r| api_model::UserKey { + id: r.id, + kind: r.kind, + data: r.data, + comment: r.comment, + }) + .try_collect::<Vec<_>>() + .await + .unwrap(); + + let count = sqlx::query!( + "SELECT COUNT(id) AS count FROM user_keys WHERE user=?", + session.user_id, + ) + .fetch_one(&mut **db) + .map_ok(|r| r.count) + .await + .unwrap(); + + let u32_count = u32::try_from(count).unwrap(); + + Json(api_model::UserKeys { + offset: uw_offset, + limit: uw_limit, + total_count: u32_count, + more: uw_offset + uw_limit < u32_count, + keys: entries, + }) +} + async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result { match Db::fetch(&rocket) { Some(db) => match sqlx::migrate!().run(&**db).await { @@ -695,6 +855,10 @@ fn rocket_from_config(figment: Figment) -> Rocket<Build> { review, review_encoded_path, users, + user_key_add, + user_key_get, + user_key_del, + user_keys, ], ) .attach(auth::stage(basepath)) diff --git a/server/src/tests.rs b/server/src/tests.rs index f1489d8..4567c49 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -208,6 +208,27 @@ async fn get_reviews<'a>(client: &Client, project_url: impl Display) -> api_mode .unwrap() } +async fn get_user_key_from<'a>(request: LocalRequest<'a>) -> api_model::UserKey { + request + .header(&FAKE_IP) + .dispatch() + .await + .into_json::<api_model::UserKey>() + .await + .unwrap() +} + +async fn get_user_keys<'a>(client: &Client) -> api_model::UserKeys { + client + .get("/api/v1/user/keys") + .header(&FAKE_IP) + .dispatch() + .await + .into_json::<api_model::UserKeys>() + .await + .unwrap() +} + #[rocket::async_test] async fn test_not_logged_in_status() { let client = async_client_with_private_database(function_name!().to_string()).await; @@ -818,3 +839,91 @@ async fn test_project_reviews_unknown_project() { .await; assert_eq!(reviews.status(), Status::NotFound); } + +#[rocket::async_test] +async fn test_user_keys_empty() { + let client = async_client_with_private_database(function_name!().to_string()).await; + + login(&client).await; + + let user_keys = get_user_keys(&client).await; + assert_eq!(user_keys.total_count, 0); + assert_eq!(user_keys.more, false); + assert_eq!(user_keys.keys.len(), 0); +} + +#[rocket::async_test] +async fn test_user_keys_add() { + let client = async_client_with_private_database(function_name!().to_string()).await; + + login(&client).await; + + let key1 = get_user_key_from(client.post("/api/v1/user/keys/add").json( + &api_model::UserKeyData { + kind: "kind", + data: "data", + comment: None, + }, + )) + .await; + + assert_eq!(key1.kind, "kind"); + assert_eq!(key1.data, "data"); + assert_eq!(key1.comment, ""); + + let mut user_keys = get_user_keys(&client).await; + assert_eq!(user_keys.total_count, 1); + assert_eq!(user_keys.more, false); + assert_eq!(user_keys.keys.len(), 1); + let key2 = user_keys.keys.get(0).unwrap(); + assert_eq!(*key2, key1); + + let key3 = get_user_key_from(client.get(format!("/api/v1/user/keys/{}", key1.id))).await; + assert_eq!(key3, key1); + + let key4 = get_user_key_from(client.post("/api/v1/user/keys/add").json( + &api_model::UserKeyData { + kind: "another kind", + data: "more data", + comment: Some("comment"), + }, + )) + .await; + + user_keys = get_user_keys(&client).await; + assert_eq!(user_keys.total_count, 2); + assert_eq!(user_keys.more, false); + assert_eq!(user_keys.keys.len(), 2); + let key5 = user_keys.keys.get(0).unwrap(); + let key6 = user_keys.keys.get(1).unwrap(); + assert_eq!(*key5, key1); + assert_eq!(*key6, key4); +} + +#[rocket::async_test] +async fn test_user_keys_del() { + let client = async_client_with_private_database(function_name!().to_string()).await; + + login(&client).await; + + let key = get_user_key_from(client.post("/api/v1/user/keys/add").json( + &api_model::UserKeyData { + kind: "kind", + data: "data", + comment: None, + }, + )) + .await; + + let delete = client + .delete(format!("/api/v1/user/keys/{}", key.id)) + .header(&FAKE_IP) + .dispatch() + .await; + assert_eq!(delete.status(), Status::Ok); + + let user_keys = get_user_keys(&client).await; + assert_eq!(user_keys.total_count, 0); + assert_eq!(user_keys.more, false); + assert_eq!(user_keys.keys.len(), 0); +} |
