Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
httpd: Add issue endpoints
Sebastian Martinez committed 3 years ago
commit 2335bc7ecc46336883075840d1cf59c23c668f4f
parent 811b70a5cbd9a6e220ab7e5efa8b0b182908a28d
4 files changed +105 -4
modified radicle-httpd/Cargo.toml
@@ -17,6 +17,7 @@ logfmt = [
[dependencies]
anyhow = { version = "1" }
axum = { version = "0.6.2", default-features = false, features = ["headers", "json", "query", "tokio"] }
+
axum-auth = { version= "0.4.0", default-features = false, features = ["auth-bearer"] }
axum-server = { version = "0.4.4", default-features = false }
chrono = { version = "0.4.22" }
fastrand = { version = "1.7.0" }
modified radicle-httpd/src/api/error.rs
@@ -30,6 +30,10 @@ pub enum Error {
    #[error(transparent)]
    Storage(#[from] radicle::storage::Error),

+
    /// Cob issue error.
+
    #[error(transparent)]
+
    CobIssue(#[from] radicle::cob::issue::Error),
+

    /// Cob store error.
    #[error(transparent)]
    CobStore(#[from] radicle::cob::store::Error),
modified radicle-httpd/src/api/json.rs
@@ -85,6 +85,7 @@ pub(crate) fn issue(id: IssueId, issue: Issue) -> Value {
    json!({
        "id": id.to_string(),
        "author": issue.author(),
+
        "assignees": issue.assigned().collect::<Vec<_>>(),
        "title": issue.title(),
        "state": issue.state(),
        "discussion": issue.comments().collect::<Comments>(),
modified radicle-httpd/src/api/v1/projects.rs
@@ -4,15 +4,17 @@ use axum::extract::State;
use axum::handler::Handler;
use axum::http::{header, HeaderValue};
use axum::response::IntoResponse;
-
use axum::routing::get;
+
use axum::routing::{get, patch, post};
use axum::{Json, Router};
+
use axum_auth::AuthBearer;
use hyper::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tower_http::set_header::SetResponseHeaderLayer;

-
use radicle::cob::issue::Issues;
+
use radicle::cob::issue::{Action, Issues};
use radicle::cob::patch::Patches;
+
use radicle::cob::{thread, Tag};
use radicle::identity::Id;
use radicle::node::NodeId;
use radicle::storage::{git::paths, ReadRepository, WriteStorage};
@@ -46,8 +48,14 @@ pub fn router(ctx: Context) -> Router {
        .route("/projects/:project/remotes/:peer", get(remote_handler))
        .route("/projects/:project/blob/:sha/*path", get(blob_handler))
        .route("/projects/:project/readme/:sha", get(readme_handler))
-
        .route("/projects/:project/issues", get(issues_handler))
-
        .route("/projects/:project/issues/:id", get(issue_handler))
+
        .route(
+
            "/projects/:project/issues",
+
            post(issue_create_handler).get(issues_handler),
+
        )
+
        .route(
+
            "/projects/:project/issues/:id",
+
            patch(issue_update_handler).get(issue_handler),
+
        )
        .route("/projects/:project/patches", get(patches_handler))
        .route("/projects/:project/patches/:id", get(patch_handler))
        .with_state(ctx)
@@ -391,6 +399,92 @@ async fn issues_handler(
    Ok::<_, Error>(Json(issues))
}

+
#[derive(Debug, Deserialize, Serialize)]
+
pub struct IssueCreate {
+
    pub title: String,
+
    pub description: String,
+
    pub tags: Vec<Tag>,
+
}
+

+
/// Create a new issue.
+
/// `POST /projects/:project/issues`
+
async fn issue_create_handler(
+
    State(ctx): State<Context>,
+
    AuthBearer(token): AuthBearer,
+
    Path(project): Path<Id>,
+
    Json(issue): Json<IssueCreate>,
+
) -> impl IntoResponse {
+
    let sessions = ctx.sessions.read().await;
+
    sessions.get(&token).ok_or(Error::Auth("Unauthorized"))?;
+
    let storage = &ctx.profile.storage;
+
    let signer = ctx
+
        .profile
+
        .signer()
+
        .map_err(|_| Error::Auth("Unauthorized"))?;
+
    let repo = storage.repository(project)?;
+
    let mut issues = Issues::open(ctx.profile.public_key, &repo)?;
+
    issues
+
        .create(issue.title, issue.description, &issue.tags, &signer)
+
        .map_err(Error::from)?;
+

+
    Ok::<_, Error>((StatusCode::CREATED, Json(json!({ "success": true }))))
+
}
+

+
/// Update an issue.
+
/// `PATCH /projects/:project/issues/:id`
+
async fn issue_update_handler(
+
    State(ctx): State<Context>,
+
    AuthBearer(token): AuthBearer,
+
    Path((project, issue_id)): Path<(Id, Oid)>,
+
    Json(action): Json<Action>,
+
) -> impl IntoResponse {
+
    let sessions = ctx.sessions.write().await;
+
    sessions.get(&token).ok_or(Error::Auth("Unauthorized"))?;
+
    let storage = &ctx.profile.storage;
+
    let signer = ctx.profile.signer().unwrap();
+
    let repo = storage.repository(project)?;
+
    let mut issues = Issues::open(ctx.profile.public_key, &repo)?;
+
    let mut issue = issues.get_mut(&issue_id.into())?;
+
    match action {
+
        Action::Assign { add, remove } => {
+
            issue.assign(add, &signer)?;
+
            issue.unassign(remove, &signer)?;
+
        }
+
        Action::Lifecycle { state } => {
+
            issue.lifecycle(state, &signer)?;
+
        }
+
        Action::Tag { add, remove } => {
+
            issue.tag(add, remove, &signer)?;
+
        }
+
        Action::Edit { title } => {
+
            issue.edit(title, &signer)?;
+
        }
+
        Action::Thread { action } => {
+
            let mut actor = thread::Actor::new(ctx.profile.signer().unwrap());
+
            match action {
+
                thread::Action::Comment { body, reply_to } => {
+
                    if let Some(reply_to) = reply_to {
+
                        issue.comment(body, reply_to, &signer)?;
+
                    } else {
+
                        issue.thread(body, &signer)?;
+
                    }
+
                }
+
                thread::Action::React { to, reaction, .. } => {
+
                    issue.react(to, reaction, &signer)?;
+
                }
+
                thread::Action::Edit { id, body } => {
+
                    actor.edit(id, &body);
+
                }
+
                thread::Action::Redact { id } => {
+
                    actor.redact(id);
+
                }
+
            }
+
        }
+
    };
+

+
    Ok::<_, Error>(Json(json!({ "success": true })))
+
}
+

/// Get project issue.
/// `GET /projects/:project/issues/:id`
async fn issue_handler(
@@ -909,6 +1003,7 @@ mod routes {
                "state": {
                    "status": "open"
                },
+
                "assignees": [],
                "discussion": [
                  {
                    "author": {