A PLC Mirror written in Rust
1use crate::types::{DidDocument, JsonCid, PlcEntry, PlcOperationType};
2use crate::{ApiContext, db};
3use dropshot::{
4 Body, ClientErrorStatusCode, HttpError, HttpResponseOk, Path, RequestContext, endpoint,
5};
6use hyper::Response;
7use ipld_core::cid::Cid;
8use schemars::JsonSchema;
9use serde::Deserialize;
10use std::str::FromStr;
11
12fn err_did_not_found(did: &str) -> HttpError {
13 HttpError::for_not_found(None, format!("DID not registered: {did}"))
14}
15
16fn err_did_tombstoned(did: &str) -> HttpError {
17 HttpError::for_client_error(
18 None,
19 ClientErrorStatusCode::GONE,
20 format!("DID not available: {did}"),
21 )
22}
23
24#[endpoint{
25 method = GET,
26 path = "/",
27}]
28// strictly this isn't the correct type, but it works
29pub async fn index(_rqctx: RequestContext<ApiContext>) -> Result<Response<Body>, HttpError> {
30 Ok(Response::builder()
31 .status(200)
32 .header("Content-Type", "text/plain")
33 .body(Body::with_content(include_str!("./index.txt")))?)
34}
35
36#[derive(Debug, JsonSchema, Deserialize)]
37pub struct DidPathParams {
38 pub did: String,
39}
40
41#[endpoint {
42 method = GET,
43 path = "/{did}",
44}]
45pub async fn resolve_did(
46 rqctx: RequestContext<ApiContext>,
47 path: Path<DidPathParams>,
48) -> Result<HttpResponseOk<DidDocument>, HttpError> {
49 let conn = rqctx.context().get_conn().await?;
50 let did = path.into_inner().did;
51
52 if did == "favicon.ico" {
53 return Err(HttpError::for_not_found(None, Default::default()));
54 }
55
56 if !did.starts_with("did:plc:") {
57 return Err(HttpError::for_bad_request(None, "Invalid DID".to_string()));
58 }
59
60 let op = db::get_latest_operation(&conn, &did)
61 .await
62 .map_err(|v| HttpError::for_internal_error(v.to_string()))?
63 .ok_or(err_did_not_found(&did))?;
64
65 let decoded: PlcOperationType = serde_json::from_value(op.operation)
66 .map_err(|v| HttpError::for_internal_error(v.to_string()))?;
67
68 let doc = match decoded {
69 PlcOperationType::Tombstone(_) => return Err(err_did_tombstoned(&did)),
70 PlcOperationType::Create(op) => DidDocument::from_create(&did, op),
71 PlcOperationType::Operation(op) => DidDocument::from_plc_op(&did, op),
72 };
73
74 Ok(HttpResponseOk(doc))
75}
76
77#[endpoint {
78 method = GET,
79 path = "/{did}/log",
80}]
81pub async fn get_plc_op_log(
82 rqctx: RequestContext<ApiContext>,
83 path: Path<DidPathParams>,
84) -> Result<HttpResponseOk<Vec<PlcOperationType>>, HttpError> {
85 let conn = rqctx.context().get_conn().await?;
86 let did = path.into_inner().did;
87
88 let ops = db::get_operations(&conn, &did)
89 .await
90 .map_err(|v| HttpError::for_internal_error(v.to_string()))?;
91
92 if ops.is_empty() {
93 return Err(err_did_not_found(&did));
94 }
95
96 let ops = ops
97 .into_iter()
98 .map(|op| serde_json::from_value(op.operation))
99 .collect::<Result<Vec<_>, _>>()
100 .map_err(|v| HttpError::for_internal_error(v.to_string()))?;
101
102 Ok(HttpResponseOk(ops))
103}
104
105#[endpoint {
106 method = GET,
107 path = "/{did}/log/audit",
108}]
109pub async fn get_plc_audit_log(
110 rqctx: RequestContext<ApiContext>,
111 path: Path<DidPathParams>,
112) -> Result<HttpResponseOk<Vec<PlcEntry>>, HttpError> {
113 let conn = rqctx.context().get_conn().await?;
114 let did = path.into_inner().did;
115
116 let ops = db::get_operations(&conn, &did)
117 .await
118 .map_err(|v| HttpError::for_internal_error(v.to_string()))?;
119
120 if ops.is_empty() {
121 return Err(err_did_not_found(&did));
122 }
123
124 let entries = ops
125 .into_iter()
126 .map(|op| {
127 let operation = serde_json::from_value(op.operation)?;
128 let cid = Cid::from_str(&op.hash).unwrap();
129
130 let entry = PlcEntry {
131 did: did.clone(),
132 operation,
133 cid: JsonCid(cid),
134 nullified: op.nullified,
135 created_at: op.created_at,
136 };
137
138 Ok(entry)
139 })
140 .collect::<Result<Vec<_>, _>>()
141 .map_err(|v: eyre::Report| HttpError::for_internal_error(v.to_string()))?;
142
143 Ok(HttpResponseOk(entries))
144}
145
146#[endpoint {
147 method = GET,
148 path = "/{did}/log/last",
149}]
150pub async fn get_last_op(
151 rqctx: RequestContext<ApiContext>,
152 path: Path<DidPathParams>,
153) -> Result<HttpResponseOk<PlcOperationType>, HttpError> {
154 let conn = rqctx.context().get_conn().await?;
155 let did = path.into_inner().did;
156
157 let op = db::get_latest_operation(&conn, &did)
158 .await
159 .map_err(|v| HttpError::for_internal_error(v.to_string()))?
160 .ok_or(err_did_not_found(&did))?;
161
162 let decoded = serde_json::from_value(op.operation)
163 .map_err(|v| HttpError::for_internal_error(v.to_string()))?;
164
165 Ok(HttpResponseOk(decoded))
166}