Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions src/main/java/com/doubleo/didagent/agent/HospitalAgent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.doubleo.didagent.agent;

import com.doubleo.didagent.agent.client.AcapyClient;
import com.doubleo.didagent.dto.request.hospital.HospitalDidCreateRequest;
import com.doubleo.didagent.dto.request.hospital.HospitalInvitationCreateRequest;
import com.doubleo.didagent.dto.request.hospital.HospitalVcIssueRequest;
import com.doubleo.didagent.dto.response.hospital.*;
import com.doubleo.didagent.infra.config.acapy.AcapyProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

@Service
@RequiredArgsConstructor
@Slf4j
public class HospitalAgent {

private final AcapyClient hospitalClient;
private final AcapyProperties acapyProperties;

public Mono<HospitalInvitationCreateResponse> createHospitalInvitation(
HospitalInvitationCreateRequest request, String token) {
log.debug("Creating hospital invitation with path: {}", acapyProperties.createInvitation());
return hospitalClient
.getWebClient()
.post()
.uri(uriBuilder -> uriBuilder.path(acapyProperties.createInvitation()).build())
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + token)
.bodyValue(request)
.retrieve()
.bodyToMono(HospitalInvitationCreateResponse.class)
.doOnError(
error -> {
log.error(
"Hospital MemberConnection fetch error: {}",
error.getMessage());
});
}

public Mono<HospitalDidCreateResponse> createHospitalDid(
HospitalDidCreateRequest request, String token) {
return hospitalClient
.getWebClient()
.post()
.uri(uriBuilder -> uriBuilder.path(acapyProperties.createDid()).build())
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + token)
.bodyValue(request)
.retrieve()
.bodyToMono(HospitalDidCreateResponse.class)
.doOnError(
error -> {
log.error("Hospital Did Creation error: {}", error.getMessage());
});
}

public Mono<HospitalDidPostResponse> postHospitalDid(String token, String did) {
return hospitalClient
.getWebClient()
.post()
.uri(
uriBuilder ->
uriBuilder
.path(acapyProperties.postPublicDid())
.queryParam("did", did)
.build())
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + token)
.retrieve()
.bodyToMono(HospitalDidPostResponse.class)
.doOnError(
error -> {
log.error("Hospital DID Post error: {}", error.getMessage());
});
}

public Mono<HospitalVcIssueResponse> issueHospitalVc(
HospitalVcIssueRequest request, String token) {
log.debug("Issue VC endpoint: {}", acapyProperties.issueVc());
Mono<HospitalVcIssueResponse> res =
hospitalClient
.getWebClient()
.post()
.uri(uriBuilder -> uriBuilder.path(acapyProperties.issueVc()).build())
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + token)
.bodyValue(request)
.retrieve()
.bodyToMono(HospitalVcIssueResponse.class)
.doOnError(
error -> {
log.error("Hospital VC Issuance error: {}", error.getMessage());
})
.doOnNext(
response ->
log.info("Hospital VC Issuance response: {}", response));
return res;
}
}
38 changes: 38 additions & 0 deletions src/main/java/com/doubleo/didagent/agent/MediatorAgent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.doubleo.didagent.agent;

import com.doubleo.didagent.agent.client.AcapyClient;
import com.doubleo.didagent.dto.request.mediator.MediatorInvitationCreateRequest;
import com.doubleo.didagent.dto.response.mediator.MediatorInvitationCreateResponse;
import com.doubleo.didagent.infra.config.acapy.AcapyProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

@Service
@RequiredArgsConstructor
@Slf4j
public class MediatorAgent {

private final AcapyClient mediatorClient;
private final AcapyProperties acapyProperties;

public Mono<MediatorInvitationCreateResponse> createMediatorInvitation(
MediatorInvitationCreateRequest request) {
return mediatorClient
.getWebClient()
.post()
.uri(uriBuilder -> uriBuilder.path(acapyProperties.createInvitation()).build())
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(request)
.retrieve()
.bodyToMono(MediatorInvitationCreateResponse.class)
.doOnError(
error -> {
log.error(
"Mediator MemberConnection fetch error: {}",
error.getMessage());
});
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,78 @@
package com.doubleo.didagent.controller;

import com.doubleo.didagent.service.AcapyWebhookService;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/webhooks")
@Slf4j
@RequiredArgsConstructor
public class AcapyWebhookController {

@PostMapping("/**")
private final AcapyWebhookService acapyWebhookService;

@PostMapping("/topic/connections/")
@ResponseStatus(HttpStatus.OK)
public void receiveAnyWebhook(
public void connectionWebhookReceive(
HttpServletRequest req,
@RequestBody Map<String, Object> payload,
@RequestHeader Map<String, String> headers) {

log.info("=== Connection Webhook Received ===");
logWebhookDetails(req, headers, payload);

try {
Mono<Void> mono = acapyWebhookService.processConnectionWebhook(payload);
mono.subscribe(null, err -> log.error("processing failed", err));
} catch (Exception e) {
log.error("Error processing connection webhook: {}", e.getMessage(), e);
}
}

@PostMapping("/topic/issue_credential_v2_0/")
@ResponseStatus(HttpStatus.OK)
public void credentialWebhookReceive(
HttpServletRequest req,
@RequestBody Map<String, Object> payload,
@RequestHeader Map<String, String> headers) {

log.info("=== Credential Webhook Received ===");
logWebhookDetails(req, headers, payload);

try {
Mono<Void> mono = acapyWebhookService.processCredentialWebhook(payload);
mono.subscribe(null, err -> log.error("processing failed", err));
} catch (Exception e) {
log.error("Error processing credential webhook: {}", e.getMessage(), e);
}
}

@PostMapping("/topic/out_of_band/")
@ResponseStatus(HttpStatus.OK)
public void outOfBandWebhookReceive(
HttpServletRequest req,
@RequestBody Map<String, Object> payload,
@RequestHeader Map<String, String> headers) {

log.info("=== Out of Band Webhook Received ===");
logWebhookDetails(req, headers, payload);

try {
Mono<Void> mono = acapyWebhookService.processOutOfBandWebhook(payload);
mono.subscribe(null, err -> log.error("processing failed", err));
} catch (Exception e) {
log.error("Error processing out of band webhook: {}", e.getMessage(), e);
}
}

private void logWebhookDetails(
HttpServletRequest req, Map<String, String> headers, Map<String, Object> payload) {
String path = req.getRequestURI();
log.info("=== Webhook Path: {} ===", path);
log.info("--- Headers ---");
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.doubleo.didagent.dto.request.hospital;

public record HospitalDidCreateRequest(String method, Options options) {
public record Options(String key_type) {}

public static HospitalDidCreateRequest didKey() {
Options options = new Options("ed25519");
return new HospitalDidCreateRequest("key", options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.doubleo.didagent.dto.request.hospital;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;

public record HospitalVcIssueRequest(
@JsonProperty("connection_id") String connectionId,
@JsonProperty("credential_preview") CredentialPreview credentialPreview,
Filter filter,
@JsonProperty("auto_issue") boolean autoIssue,
@JsonProperty("auto_remove") boolean autoRemove) {

public record CredentialPreview(
@JsonProperty("@type") String type, List<Attribute> attributes) {
public record Attribute(String name, String value) {}
}

public record Filter(@JsonProperty("ld_proof") LdProofFilter ldProof) {}

public record LdProofFilter(
@JsonProperty("credential") Credential credential,
@JsonProperty("options") Options options) {}

public record Credential(
@JsonProperty("@context") List<Object> context,
@JsonProperty("type") List<String> type,
@JsonProperty("issuer") String issuer,
@JsonProperty("issuanceDate") String issuanceDate,
@JsonProperty("credentialSubject") CredentialSubject credentialSubject) {}

public record CredentialSubject(
@JsonProperty("id") String id,
@JsonProperty("hospital_tenant") String hospitalTenant,
@JsonProperty("area_code") String areaCode) {}

public record Options(
@JsonProperty("proofType") String proofType,
@JsonProperty("proofPurpose") String proofPurpose) {}

// LD Proof 기반 병원 μ ‘κ·Ό ν¬λ¦¬λ΄μ…œμš© νŒ©ν† λ¦¬ λ©”μ„œλ“œ
public static HospitalVcIssueRequest createLdProofCredential(
String connectionId,
String issuer,
String credentialSubjectId,
String hospitalTenant,
String areaCode) {

List<Object> defaultContext =
List.of(
"https://www.w3.org/2018/credentials/v1",
Map.of(
"hospital_tenant", "https://example.org/hospital_tenant",
"area_code", "https://example.org/area_code"));

List<String> defaultType = List.of("VerifiableCredential", "HospitalAccessCredential");
String defaultIssuanceDate = java.time.Instant.now().toString();
String defaultProofType = "Ed25519Signature2018";
String defaultProofPurpose = "assertionMethod";

// Credential Preview 생성
CredentialPreview preview =
new CredentialPreview(
"https://didcomm.org/issue-credential/2.0/credential-preview",
List.of(
new CredentialPreview.Attribute("hospital_tenant", hospitalTenant),
new CredentialPreview.Attribute("area_code", areaCode)));

// Credential Subject 생성
CredentialSubject credentialSubject =
new CredentialSubject(credentialSubjectId, hospitalTenant, areaCode);

// LD Proof Filter 생성
LdProofFilter ldProofFilter =
new LdProofFilter(
new Credential(
defaultContext,
defaultType,
issuer,
defaultIssuanceDate,
credentialSubject),
new Options(defaultProofType, defaultProofPurpose));

// Filter 생성 (LD Proof만 μ‚¬μš©)
Filter filter = new Filter(ldProofFilter);

return new HospitalVcIssueRequest(
connectionId,
preview,
filter,
true, // auto_issue
false // auto_remove
);
}

// κ°„λ‹¨ν•œ 버전 (κΈ°λ³Έκ°’ μ‚¬μš©)
public static HospitalVcIssueRequest createWithDid(
String connectionId, String issuer, String credentialSubjectId) {

return createLdProofCredential(
connectionId,
issuer,
credentialSubjectId,
"default_hospital", // κΈ°λ³Έ 병원 ν…λ„ŒνŠΈ
"default_area" // κΈ°λ³Έ μ§€μ—­ μ½”λ“œ
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.doubleo.didagent.dto.request.mediator;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

public record MediatorInvitationCreateRequest(
@JsonProperty("alias") String alias,
@JsonProperty("handshake_protocols") List<String> handshakeProtocols,
@JsonProperty("goal_code") String goalCode,
@JsonProperty("my_label") String myLabel,
@JsonProperty("accept") List<String> accept,
@JsonProperty("use_did_method") String useDidMethod,
@JsonProperty("multi_use") boolean multiUse) {
public static MediatorInvitationCreateRequest generate() {
List<String> handshakeProtocols = new ArrayList<>();
List<String> accept = new ArrayList<>();

handshakeProtocols.add("https://didcomm.org/didexchange/1.0");
accept.add("didcomm/aip2;env=rfc19");

return new MediatorInvitationCreateRequest(
"mediator:invitation:" + LocalDateTime.now(),
handshakeProtocols,
"vc-issue",
"keywe_mediator",
accept,
"did:peer:2",
true);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.doubleo.didagent.dto.response.hospital;

public record HospitalConnectionInfoResponse() {}
Loading