diff --git a/radicle-httpd/src/api.rs b/radicle-httpd/src/api.rs index b3708d819..75fb62183 100644 --- a/radicle-httpd/src/api.rs +++ b/radicle-httpd/src/api.rs @@ -125,10 +125,20 @@ async fn root_handler() -> impl IntoResponse { #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct PaginationQuery { + #[serde(default)] + pub show: ProjectQuery, pub page: Option, pub per_page: Option, } +#[derive(Serialize, Deserialize, Clone, Default)] +#[serde(rename_all = "camelCase")] +pub enum ProjectQuery { + All, + #[default] + Pinned, +} + #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct RawQuery { diff --git a/radicle-httpd/src/api/v1/delegates.rs b/radicle-httpd/src/api/v1/delegates.rs index 260907c92..d7924df38 100644 --- a/radicle-httpd/src/api/v1/delegates.rs +++ b/radicle-httpd/src/api/v1/delegates.rs @@ -14,7 +14,7 @@ use radicle::storage::{ReadRepository, ReadStorage}; use crate::api::error::Error; use crate::api::project::Info; use crate::api::Context; -use crate::api::PaginationQuery; +use crate::api::{PaginationQuery, ProjectQuery}; use crate::axum_extra::{Path, Query}; pub fn router(ctx: Context) -> Router { @@ -34,19 +34,28 @@ async fn delegates_projects_handler( Path(delegate): Path, Query(qs): Query, ) -> impl IntoResponse { - let PaginationQuery { page, per_page } = qs; + let PaginationQuery { + show, + page, + per_page, + } = qs; let page = page.unwrap_or(0); let per_page = per_page.unwrap_or(10); let storage = &ctx.profile.storage; let db = &ctx.profile.database()?; - let mut projects = storage - .repositories()? - .into_iter() - .filter(|id| match &id.doc.visibility { - Visibility::Private { .. } => addr.ip().is_loopback(), - Visibility::Public => true, - }) - .collect::>(); + let pinned = &ctx.profile.config.web.pinned; + + let mut projects = match show { + ProjectQuery::All => storage + .repositories()? + .into_iter() + .filter(|repo| match &repo.doc.visibility { + Visibility::Private { .. } => addr.ip().is_loopback(), + Visibility::Public => true, + }) + .collect::>(), + ProjectQuery::Pinned => storage.repositories_by_id(pinned.repositories.iter())?, + }; projects.sort_by_key(|p| p.rid); let infos = projects @@ -116,11 +125,16 @@ mod routes { .layer(MockConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080)))); let response = get( &app, - "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects", + "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects?show=all", ) .await; - assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.status(), + StatusCode::OK, + "failed response: {:?}", + response.json().await + ); assert_eq!( response.json().await, json!([ @@ -179,11 +193,16 @@ mod routes { )))); let response = get( &app, - "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects", + "/delegates/did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/projects?show=all", ) .await; - assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.status(), + StatusCode::OK, + "failed response: {:?}", + response.json().await + ); assert_eq!( response.json().await, json!([ diff --git a/radicle-httpd/src/api/v1/profile.rs b/radicle-httpd/src/api/v1/profile.rs index ce703c0dd..ccca4a401 100644 --- a/radicle-httpd/src/api/v1/profile.rs +++ b/radicle-httpd/src/api/v1/profile.rs @@ -75,6 +75,7 @@ mod routes { "preferredSeeds": [ "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776" ], + "web": { "pinned": { "repositories": [] } }, "cli": { "hints": true }, diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index ac4677fe9..e10bfdb60 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -24,7 +24,7 @@ use radicle_surf::{diff, Glob, Oid, Repository}; use crate::api::error::Error; use crate::api::project::Info; -use crate::api::{self, announce_refs, CobsQuery, Context, PaginationQuery}; +use crate::api::{self, announce_refs, CobsQuery, Context, PaginationQuery, ProjectQuery}; use crate::axum_extra::{Path, Query}; const CACHE_1_HOUR: &str = "public, max-age=3600, must-revalidate"; @@ -79,19 +79,28 @@ async fn project_root_handler( State(ctx): State, Query(qs): Query, ) -> impl IntoResponse { - let PaginationQuery { page, per_page } = qs; + let PaginationQuery { + show, + page, + per_page, + } = qs; let page = page.unwrap_or(0); let per_page = per_page.unwrap_or(10); let storage = &ctx.profile.storage; let db = &ctx.profile.database()?; - let mut projects = storage - .repositories()? - .into_iter() - .filter(|id| match &id.doc.visibility { - Visibility::Private { .. } => addr.ip().is_loopback(), - Visibility::Public => true, - }) - .collect::>(); + let pinned = &ctx.profile.config.web.pinned; + + let mut projects = match show { + ProjectQuery::All => storage + .repositories()? + .into_iter() + .filter(|repo| match &repo.doc.visibility { + Visibility::Private { .. } => addr.ip().is_loopback(), + Visibility::Public => true, + }) + .collect::>(), + ProjectQuery::Pinned => storage.repositories_by_id(pinned.repositories.iter())?, + }; projects.sort_by_key(|p| p.rid); let infos = projects @@ -976,7 +985,7 @@ mod routes { let seed = seed(tmp.path()); let app = super::router(seed.clone()) .layer(MockConnectInfo(SocketAddr::from(([127, 0, 0, 1], 8080)))); - let response = get(&app, "/projects").await; + let response = get(&app, "/projects?show=all").await; assert_eq!(response.status(), StatusCode::OK); assert_eq!( @@ -1035,7 +1044,7 @@ mod routes { [192, 168, 13, 37], 8080, )))); - let response = get(&app, "/projects").await; + let response = get(&app, "/projects?show=all").await; assert_eq!(response.status(), StatusCode::OK); assert_eq!(