summaryrefslogtreecommitdiff
path: root/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'server/src')
-rw-r--r--server/src/api_model.rs35
-rw-r--r--server/src/main.rs164
-rw-r--r--server/src/tests.rs109
3 files changed, 308 insertions, 0 deletions
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);
+}