Skip to content

Commit

Permalink
feat: upload holder credentials via HTTP API (#141)
Browse files Browse the repository at this point in the history
* feat: create Presentations using raw Credentials

* fix: undo changes to `POST` presentation endpoint

* feat: add `POST` method for `/holder/credentials`

* docs: add `Add signed Holder Credential` request to Postman Collection
  • Loading branch information
nanderstabel authored Dec 12, 2024
1 parent 8daca06 commit 8c8c246
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 12 deletions.
39 changes: 39 additions & 0 deletions agent_api_rest/postman/ssi-agent.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,45 @@
},
"response": []
},
{
"name": "Add signed Holder Credential",
"event": [
{
"listen": "test",
"script": {
"exec": [
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"credential\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1raWlleW9MTVNWc0pBWnY3SmplNXdXU2tERXltVWdreUY4a2JjcmpacFgzcWQiLCJmaXJzdF9uYW1lIjoiRmVycmlzIiwibGFzdF9uYW1lIjoiUnVzdGFjZWFuIn0sImlzc3VlciI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQwMDowMDowMFoifX0.d4QN73vDtZu79RP6GldHObu6rGsjidkLYp0XMRQNbNPY75LJoSv2iXk2Rz5M-VMBZGSU3YPZHytlrKBjxr1IBQ\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{HOST}}/v0/holder/credentials",
"host": [
"{{HOST}}"
],
"path": [
"v0",
"holder",
"credentials"
]
}
},
"response": []
},
{
"name": "Holder Credential by ID",
"request": {
Expand Down
45 changes: 42 additions & 3 deletions agent_api_rest/src/holder/holder/credentials/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use agent_holder::state::HolderState;
use agent_shared::handlers::query_handler;
use agent_holder::{credential::command::CredentialCommand, state::HolderState};
use agent_shared::handlers::{command_handler, query_handler};
use axum::{
extract::{Path, State},
response::{IntoResponse, Response},
Json,
};
use hyper::StatusCode;
use serde_json::json;
use identity_credential::credential::Jwt;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tracing::info;

#[axum_macros::debug_handler]
pub(crate) async fn credentials(State(state): State<HolderState>) -> Response {
Expand All @@ -21,6 +24,42 @@ pub(crate) async fn credentials(State(state): State<HolderState>) -> Response {
}
}

#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HolderCredentialsEndpointRequest {
pub credential: Jwt,
}

#[axum_macros::debug_handler]
pub(crate) async fn post_credentials(State(state): State<HolderState>, Json(payload): Json<Value>) -> Response {
info!("Request Body: {}", payload);

let Ok(HolderCredentialsEndpointRequest { credential }) = serde_json::from_value(payload) else {
return (StatusCode::BAD_REQUEST, "invalid payload").into_response();
};

let holder_credential_id = uuid::Uuid::new_v4().to_string();

let command = CredentialCommand::AddCredential {
holder_credential_id: holder_credential_id.clone(),
received_offer_id: None,
credential,
};

// Add the credential.
if command_handler(&holder_credential_id, &state.command.credential, command)
.await
.is_err()
{
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}

match query_handler(&holder_credential_id, &state.query.holder_credential).await {
Ok(Some(holder_credential_view)) => (StatusCode::OK, Json(holder_credential_view)).into_response(),
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}

#[axum_macros::debug_handler]
pub(crate) async fn credential(State(state): State<HolderState>, Path(holder_credential_id): Path<String>) -> Response {
match query_handler(&holder_credential_id, &state.query.holder_credential).await {
Expand Down
2 changes: 1 addition & 1 deletion agent_api_rest/src/holder/holder/offers/accept.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub(crate) async fn accept(State(state): State<HolderState>, Path(received_offer
{
let command = CredentialCommand::AddCredential {
holder_credential_id: holder_credential_id.clone(),
received_offer_id: received_offer_id.clone(),
received_offer_id: Some(received_offer_id.clone()),
credential,
};

Expand Down
4 changes: 2 additions & 2 deletions agent_api_rest/src/holder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use agent_holder::state::HolderState;
use axum::routing::get;
use axum::{routing::post, Router};
use holder::{
credentials::credential,
credentials::{credential, post_credentials},
presentations::{get_presentations, post_presentations, presentation, presentation_signed::presentation_signed},
};

Expand All @@ -21,7 +21,7 @@ pub fn router(holder_state: HolderState) -> Router {
.nest(
API_VERSION,
Router::new()
.route("/holder/credentials", get(credentials))
.route("/holder/credentials", get(credentials).post(post_credentials))
.route("/holder/credentials/:credential_id", get(credential))
.route("/holder/presentations", get(get_presentations).post(post_presentations))
.route("/holder/presentations/:presentation_id", get(presentation))
Expand Down
6 changes: 3 additions & 3 deletions agent_holder/src/credential/aggregate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ impl Aggregate for Credential {
data,
} => {
self.holder_credential_id = holder_credential_id;
self.received_offer_id = Some(received_offer_id);
self.received_offer_id = received_offer_id;
self.signed = Some(credential);
self.data = Some(data);
}
Expand Down Expand Up @@ -123,12 +123,12 @@ pub mod credential_tests {
.given_no_previous_events()
.when(CredentialCommand::AddCredential {
holder_credential_id: holder_credential_id.clone(),
received_offer_id: received_offer_id.clone(),
received_offer_id: Some(received_offer_id.clone()),
credential: Jwt::from(OPENBADGE_VERIFIABLE_CREDENTIAL_JWT.to_string()),
})
.then_expect_events(vec![CredentialEvent::CredentialAdded {
holder_credential_id,
received_offer_id,
received_offer_id: Some(received_offer_id),
credential: Jwt::from(OPENBADGE_VERIFIABLE_CREDENTIAL_JWT.to_string()),
data: Data {
raw: get_unverified_jwt_claims(&serde_json::json!(OPENBADGE_VERIFIABLE_CREDENTIAL_JWT)).unwrap()
Expand Down
2 changes: 1 addition & 1 deletion agent_holder/src/credential/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use serde::Deserialize;
pub enum CredentialCommand {
AddCredential {
holder_credential_id: String,
received_offer_id: String,
received_offer_id: Option<String>,
credential: Jwt,
},
}
2 changes: 1 addition & 1 deletion agent_holder/src/credential/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use super::aggregate::Data;
pub enum CredentialEvent {
CredentialAdded {
holder_credential_id: String,
received_offer_id: String,
received_offer_id: Option<String>,
credential: Jwt,
data: Data,
},
Expand Down
2 changes: 1 addition & 1 deletion agent_holder/src/credential/queries/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ impl View<Credential> for Credential {
data,
} => {
self.holder_credential_id.clone_from(holder_credential_id);
self.received_offer_id.replace(offer_id.clone());
self.received_offer_id.clone_from(offer_id);
self.signed.replace(credential.clone());
self.data.replace(data.clone());
}
Expand Down

0 comments on commit 8c8c246

Please sign in to comment.