1use axum::{
2 extract::{Json, Path, State},
3 http::status::StatusCode,
4 response::IntoResponse,
5};
6use hopr_lib::Address;
7use serde::{Deserialize, Serialize};
8use serde_with::{serde_as, DisplayFromStr};
9use std::{collections::HashMap, str::FromStr, sync::Arc};
10
11use hoprd_db_api::aliases::HoprdDbAliasesOperations;
12use hoprd_db_api::errors::DbError;
13
14use crate::{
15 types::{HoprIdentifier, PeerOrAddress},
16 ApiError, ApiErrorStatus, InternalState, BASE_PATH,
17};
18
19#[serde_as]
20#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
21#[schema(example = json!({
22 "peerId": "12D3KooWRWeTozREYHzWTbuCYskdYhED1MXpDwTrmccwzFrd2mEA"
23 }))]
24#[serde(rename_all = "camelCase")]
25pub(crate) struct PeerIdResponse {
26 #[serde_as(as = "DisplayFromStr")]
27 #[schema(value_type = String)]
28 pub peer_id: String,
29}
30
31#[serde_as]
32#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
33#[schema(example = json!({
34 "address": "0x07eaf07d6624f741e04f4092a755a9027aaab7f6"
35 }))]
36#[serde(rename_all = "camelCase")]
37pub(crate) struct AddressResponse {
38 #[serde_as(as = "DisplayFromStr")]
39 #[schema(value_type = String)]
40 pub address: Address,
41}
42
43#[serde_as]
44#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
45#[schema(example = json!({
46 "alias": "Alice",
47 "destination": "12D3KooWRWeTozREYHzWTbuCYskdYhED1MXpDwTrmccwzFrd2mEA"
48 }))]
49#[serde(rename_all = "camelCase")]
50pub(crate) struct AliasDestinationBodyRequest {
51 pub alias: String,
52 #[serde_as(as = "DisplayFromStr")]
53 #[schema(value_type = String)]
54 pub destination: PeerOrAddress,
55}
56
57#[utoipa::path(
59 get,
60 path = const_format::formatcp!("{BASE_PATH}/aliases"),
61 responses(
62 (status = 200, description = "Each alias with its corresponding PeerId", body = HashMap<String, String>, example = json!({
63 "alice": "12D3KooWPWD5P5ZzMRDckgfVaicY5JNoo7JywGotoAv17d7iKx1z",
64 "me": "12D3KooWJmLm8FnBfvYQ5BAZ5qcYBxQFFBzAAEYUBUNJNE8cRsYS"
65 })),
66 (status = 401, description = "Invalid authorization token.", body = ApiError),
67 (status = 404, description = "No aliases found", body = ApiError),
68 ),
69 security(
70 ("api_token" = []),
71 ("bearer_token" = [])
72 ),
73 tag = "Alias",
74 )]
75pub(super) async fn aliases(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
76 let aliases = state.hoprd_db.get_aliases().await;
77
78 match aliases {
79 Ok(aliases) => {
80 let aliases = aliases
81 .iter()
82 .map(|alias| (alias.alias.clone(), alias.peer_id.clone()))
83 .collect::<HashMap<_, _>>();
84 (StatusCode::OK, Json(aliases)).into_response()
85 }
86 Err(DbError::BackendError(_)) => (StatusCode::NOT_FOUND, ApiErrorStatus::AliasNotFound).into_response(),
87 Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, ApiErrorStatus::DatabaseError).into_response(),
88 }
89}
90
91#[utoipa::path(
93 get,
94 path = const_format::formatcp!("{BASE_PATH}/aliases_addresses"),
95 responses(
96 (status = 200, description = "Each alias with its corresponding Address", body = HashMap<String, String>, example = json!({
97 "alice": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
98 "me": "0x07eaf07d6624f741e04f4092a755a9027aaab7f6"
99 })),
100 (status = 401, description = "Invalid authorization token.", body = ApiError),
101 (status = 404, description = "No aliases found", body = ApiError),
102 ),
103 security(
104 ("api_token" = []),
105 ("bearer_token" = [])
106 ),
107 tag = "Alias",
108 )]
109pub(super) async fn aliases_addresses(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
110 let aliases = state.hoprd_db.get_aliases().await;
111
112 match aliases {
113 Ok(aliases) => {
114 let aliases = aliases
115 .into_iter()
116 .map(|alias| (alias.alias.clone(), alias.peer_id.clone()))
117 .collect::<HashMap<_, _>>();
118
119 let aliases_addresses_futures = aliases.into_iter().map(|(alias, peer_id)| {
120 let hopr = state.hopr.clone();
121 async move {
122 let peer_or_address = match PeerOrAddress::from_str(peer_id.as_str()) {
123 Ok(destination) => destination,
124 Err(_) => return (alias, ApiErrorStatus::PeerNotFound.to_string()),
125 };
126
127 match HoprIdentifier::new_with(peer_or_address, hopr.peer_resolver()).await {
128 Ok(destination) => (alias, destination.address.to_string()),
129 Err(_) => (alias, ApiErrorStatus::PeerNotFound.to_string()),
130 }
131 }
132 });
133
134 let aliases_addresses = futures::future::join_all(aliases_addresses_futures)
135 .await
136 .into_iter()
137 .collect::<HashMap<_, _>>();
138
139 (StatusCode::OK, Json(aliases_addresses)).into_response()
140 }
141 Err(DbError::BackendError(_)) => (StatusCode::NOT_FOUND, ApiErrorStatus::AliasNotFound).into_response(),
142 Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, ApiErrorStatus::DatabaseError).into_response(),
143 }
144}
145
146#[utoipa::path(
148 post,
149 path = const_format::formatcp!("{BASE_PATH}/aliases"),
150 request_body(
151 content = AliasDestinationBodyRequest,
152 description = "Alias name along with the PeerId to be aliased",
153 content_type = "application/json"),
154 responses(
155 (status = 201, description = "Alias set successfully.", body = PeerIdResponse),
156 (status = 400, description = "Invalid PeerId: The format or length of the peerId is incorrect.", body = ApiError),
157 (status = 401, description = "Invalid authorization token.", body = ApiError),
158 (status = 409, description = "Either alias exists or the peer_id is already aliased.", body = ApiError),
159 (status = 422, description = "Unknown failure", body = ApiError)
160 ),
161 security(
162 ("api_token" = []),
163 ("bearer_token" = [])
164 ),
165 tag = "Alias",
166 )]
167pub(super) async fn set_alias(
168 State(state): State<Arc<InternalState>>,
169 Json(args): Json<AliasDestinationBodyRequest>,
170) -> impl IntoResponse {
171 let hopr = state.hopr.clone();
172
173 match HoprIdentifier::new_with(args.destination, hopr.peer_resolver()).await {
174 Ok(destination) => match state
175 .hoprd_db
176 .set_alias(destination.peer_id.to_string(), args.alias)
177 .await
178 {
179 Ok(()) => (
180 StatusCode::CREATED,
181 Json(PeerIdResponse {
182 peer_id: destination.peer_id.to_string(),
183 }),
184 )
185 .into_response(),
186 Err(DbError::LogicalError(_)) => (StatusCode::BAD_REQUEST, ApiErrorStatus::PeerNotFound).into_response(),
187 Err(DbError::AliasOrPeerIdAlreadyExists) => {
188 (StatusCode::CONFLICT, ApiErrorStatus::AliasOrPeerIdAliasAlreadyExists).into_response()
189 }
190 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
191 },
192 Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
193 }
194}
195
196#[serde_as]
197#[derive(Deserialize, utoipa::ToSchema)]
198#[schema(example = json!({
199 "alias": "Alice",
200}))]
201#[serde(rename_all = "camelCase")]
202pub(crate) struct GetAliasRequest {
203 alias: String,
204}
205
206#[utoipa::path(
208 get,
209 path = const_format::formatcp!("{BASE_PATH}/aliases/{{alias}}"),
210 params(
211 ("alias" = String, Path, description = "Alias to be shown"),
212 ),
213 responses(
214 (status = 200, description = "Get PeerId for an alias", body = PeerIdResponse),
215 (status = 401, description = "Invalid authorization token.", body = ApiError),
216 (status = 404, description = "PeerId not found", body = ApiError),
217 ),
218 security(
219 ("api_token" = []),
220 ("bearer_token" = [])
221 ),
222 tag = "Alias",
223 )]
224pub(super) async fn get_alias(
225 Path(GetAliasRequest { alias }): Path<GetAliasRequest>,
226 State(state): State<Arc<InternalState>>,
227) -> impl IntoResponse {
228 let alias = urlencoding::decode(&alias);
229
230 if alias.is_err() {
231 return (StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidInput).into_response();
232 }
233
234 let alias = alias.unwrap().into_owned();
235
236 match state.hoprd_db.resolve_alias(alias.clone()).await {
237 Ok(Some(entry)) => (StatusCode::OK, Json(PeerIdResponse { peer_id: entry })).into_response(),
238 Ok(None) => (StatusCode::NOT_FOUND, ApiErrorStatus::AliasNotFound).into_response(),
239 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
240 }
241}
242
243#[utoipa::path(
245 get,
246 path = const_format::formatcp!("{BASE_PATH}/aliases_addresses/{{alias}}"),
247 params(
248 ("alias" = String, Path, description = "Alias to be shown"),
249 ),
250 responses(
251 (status = 200, description = "Get ETH address for an alias", body = AddressResponse),
252 (status = 401, description = "Invalid authorization token.", body = ApiError),
253 (status = 404, description = "Address not found", body = ApiError),
254 ),
255 security(
256 ("api_token" = []),
257 ("bearer_token" = [])
258 ),
259 tag = "Alias",
260 )]
261pub(super) async fn get_alias_address(
262 Path(GetAliasRequest { alias }): Path<GetAliasRequest>,
263 State(state): State<Arc<InternalState>>,
264) -> impl IntoResponse {
265 let hopr = state.hopr.clone();
266
267 let alias = urlencoding::decode(&alias);
268
269 if alias.is_err() {
270 return (StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidInput).into_response();
271 }
272
273 let alias = alias.unwrap().into_owned();
274
275 match state.hoprd_db.resolve_alias(alias.clone()).await {
276 Ok(Some(entry)) => {
277 let peer_or_address = match PeerOrAddress::from_str(entry.as_str()) {
278 Ok(destination) => destination,
279 Err(_) => return (StatusCode::NOT_FOUND, ApiErrorStatus::AliasNotFound).into_response(),
280 };
281
282 match HoprIdentifier::new_with(peer_or_address, hopr.peer_resolver()).await {
283 Ok(destination) => (
284 StatusCode::OK,
285 Json(AddressResponse {
286 address: destination.address,
287 }),
288 )
289 .into_response(),
290 Err(_) => (StatusCode::BAD_REQUEST, ApiErrorStatus::PeerNotFound).into_response(),
291 }
292 }
293 Ok(None) => (StatusCode::NOT_FOUND, ApiErrorStatus::AliasNotFound).into_response(),
294 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
295 }
296}
297
298#[serde_as]
299#[derive(Deserialize, utoipa::ToSchema)]
300#[schema(example = json!({
301 "alias": "Alice",
302}))]
303pub(crate) struct DeleteAliasRequest {
304 alias: String,
305}
306
307#[utoipa::path(
309 delete,
310 path = const_format::formatcp!("{BASE_PATH}/aliases/{{alias}}"),
311 params(
312 ("alias" = String, Path, description = "Alias to be shown"),
313 ),
314 responses(
315 (status = 204, description = "Alias removed successfully"),
316 (status = 401, description = "Invalid authorization token.", body = ApiError),
317 (status = 422, description = "Unknown failure", body = ApiError) ),
319 security(
320 ("api_token" = []),
321 ("bearer_token" = [])
322 ),
323 tag = "Alias",
324 )]
325pub(super) async fn delete_alias(
326 Path(DeleteAliasRequest { alias }): Path<DeleteAliasRequest>,
327 State(state): State<Arc<InternalState>>,
328) -> impl IntoResponse {
329 let alias = urlencoding::decode(&alias);
330
331 if alias.is_err() {
332 return (StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidInput).into_response();
333 }
334
335 let alias = alias.unwrap().into_owned();
336
337 match state.hoprd_db.delete_alias(alias.clone()).await {
338 Ok(_) => (StatusCode::NO_CONTENT,).into_response(),
339 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
340 }
341}
342
343#[utoipa::path(
345 delete,
346 path = const_format::formatcp!("{BASE_PATH}/aliases"),
347 responses(
348 (status = 204, description = "All aliases removed successfully"),
349 (status = 401, description = "Invalid authorization token.", body = ApiError),
350 (status = 422, description = "Unknown failure", body = ApiError) ),
352 security(
353 ("api_token" = []),
354 ("bearer_token" = [])
355 ),
356 tag = "Alias",
357 )]
358pub(super) async fn clear_aliases(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
359 match state.hoprd_db.clear_aliases().await {
360 Ok(_) => (StatusCode::NO_CONTENT,).into_response(),
361 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, ApiErrorStatus::from(e)).into_response(),
362 }
363}