1use axum::{
4 body::Bytes,
5 extract::{ConnectInfo, Path, Query, State},
6 http::StatusCode,
7 Json,
8};
9use serde::{Deserialize, Serialize};
10use std::net::SocketAddr;
11
12use cloudillo_core::extract::{Auth, IdTag, OptionalRequestId};
13use cloudillo_core::settings::SettingValue;
14use cloudillo_types::address::parse_address_type;
15use cloudillo_types::identity_provider_adapter::{
16 CreateIdentityOptions, Identity, IdentityStatus, ListIdentityOptions, UpdateIdentityOptions,
17};
18use cloudillo_types::types::{
19 serialize_timestamp_iso, serialize_timestamp_iso_opt, ApiResponse, Timestamp,
20};
21use cloudillo_types::utils::parse_and_validate_identity_id_tag;
22
23use crate::prelude::*;
24
25async fn check_idp_enabled(app: &App, tn_id: TnId) -> ClResult<()> {
27 match app.settings.get(tn_id, "idp.enabled").await {
28 Ok(SettingValue::Bool(true)) => {
29 debug!(tn_id = tn_id.0, "IDP enabled for tenant");
30 Ok(())
31 }
32 Ok(SettingValue::Bool(false)) => {
33 warn!(tn_id = tn_id.0, "IDP not enabled for tenant");
34 Err(Error::NotFound)
35 }
36 Ok(_) => {
37 warn!(tn_id = tn_id.0, "Invalid idp.enabled setting value");
38 Err(Error::ConfigError("Invalid idp.enabled setting value (expected boolean)".into()))
39 }
40 Err(e) => {
41 warn!(tn_id = tn_id.0, error = ?e, "Failed to check idp.enabled setting");
42 Err(e)
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum IdpAuthResult {
50 Owner,
52 Registrar,
54 Denied,
56}
57
58fn check_identity_access(identity: &Identity, requester_id_tag: &str) -> IdpAuthResult {
65 if let Some(ref owner) = identity.owner_id_tag {
67 if owner.as_ref() == requester_id_tag {
68 return IdpAuthResult::Owner;
69 }
70 }
71
72 if identity.registrar_id_tag.as_ref() == requester_id_tag {
74 if identity.status == IdentityStatus::Pending {
75 return IdpAuthResult::Registrar;
76 }
77 debug!(
79 identity = %format!("{}.{}", identity.id_tag_prefix, identity.id_tag_domain),
80 registrar = %identity.registrar_id_tag,
81 status = ?identity.status,
82 "Registrar denied access - identity no longer Pending"
83 );
84 }
85
86 IdpAuthResult::Denied
87}
88
89fn can_access_identity(identity: &Identity, requester_id_tag: &str) -> bool {
91 matches!(
92 check_identity_access(identity, requester_id_tag),
93 IdpAuthResult::Owner | IdpAuthResult::Registrar
94 )
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct IdentityResponse {
101 pub id_tag: String,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub email: Option<String>,
105 pub registrar_id_tag: String,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub owner_id_tag: Option<String>,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub address: Option<String>,
111 #[serde(
112 skip_serializing_if = "Option::is_none",
113 serialize_with = "serialize_timestamp_iso_opt"
114 )]
115 pub address_updated_at: Option<Timestamp>,
116 pub dyndns: bool,
118 pub status: String,
119 #[serde(serialize_with = "serialize_timestamp_iso")]
120 pub created_at: Timestamp,
121 #[serde(serialize_with = "serialize_timestamp_iso")]
122 pub updated_at: Timestamp,
123 #[serde(serialize_with = "serialize_timestamp_iso")]
124 pub expires_at: Timestamp,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub api_key: Option<String>,
128}
129
130impl From<Identity> for IdentityResponse {
131 fn from(identity: Identity) -> Self {
132 let id_tag = format!("{}.{}", identity.id_tag_prefix, identity.id_tag_domain);
134 Self {
135 id_tag,
136 email: identity.email.map(|e| e.to_string()),
137 registrar_id_tag: identity.registrar_id_tag.to_string(),
138 owner_id_tag: identity.owner_id_tag.map(|o| o.to_string()),
139 address: identity.address.map(|a| a.to_string()),
140 address_updated_at: identity.address_updated_at,
141 dyndns: identity.dyndns,
142 status: identity.status.to_string(),
143 created_at: identity.created_at,
144 updated_at: identity.updated_at,
145 expires_at: identity.expires_at,
146 api_key: None, }
148 }
149}
150
151#[derive(Debug, Deserialize)]
153#[serde(rename_all = "camelCase")]
154pub struct CreateIdentityRequest {
155 pub id_tag: String,
157 pub email: Option<String>,
159 pub owner_id_tag: Option<String>,
161 pub address: Option<String>,
163 #[serde(default)]
165 pub dyndns: bool,
166 #[serde(default = "default_true")]
169 pub send_activation_email: bool,
170 #[serde(default)]
172 pub create_api_key: bool,
173 pub api_key_name: Option<String>,
175}
176
177fn default_true() -> bool {
178 true
179}
180
181#[derive(Debug, Deserialize, Default)]
183pub struct UpdateAddressRequest {
184 #[serde(default)]
186 pub address: Option<String>,
187 #[serde(default)]
189 pub auto_address: bool,
190}
191
192#[derive(Debug, Clone, Serialize)]
194pub struct AddressUpdateResponse {
195 pub address: String,
196}
197
198fn normalize_identity_path(identity_id: &str, idp_domain: &str) -> String {
201 if identity_id.contains('.') {
202 identity_id.to_string()
204 } else {
205 format!("{}.{}", identity_id, idp_domain)
209 }
210}
211
212#[derive(Debug, Deserialize, Default)]
214#[serde(rename_all = "camelCase")]
215pub struct ListIdentitiesQuery {
216 pub email: Option<String>,
218 pub registrar_id_tag: Option<String>,
220 pub owner_id_tag: Option<String>,
222 pub status: Option<String>,
224 pub limit: Option<u32>,
226 pub offset: Option<u32>,
228}
229
230#[axum::debug_handler]
232pub async fn get_identity_by_id(
233 State(app): State<App>,
234 tn_id: TnId,
235 IdTag(idp_domain): IdTag,
236 Path(identity_id): Path<String>,
237 OptionalRequestId(req_id): OptionalRequestId,
238) -> ClResult<(StatusCode, Json<ApiResponse<IdentityResponse>>)> {
239 info!(
240 identity_id = %identity_id,
241 idp_domain = %idp_domain,
242 "GET /api/idp/identities/:id"
243 );
244
245 check_idp_enabled(&app, tn_id).await?;
247
248 let idp_adapter = app.idp_adapter.as_ref().ok_or(Error::ServiceUnavailable(
250 "Identity Provider not available on this instance".to_string(),
251 ))?;
252
253 let (id_tag_prefix, id_tag_domain) =
255 parse_and_validate_identity_id_tag(&identity_id, &idp_domain)?;
256
257 let identity = idp_adapter
259 .read_identity(&id_tag_prefix, &id_tag_domain)
260 .await?
261 .ok_or(Error::NotFound)?;
262
263 if !can_access_identity(&identity, &idp_domain) {
265 warn!(
266 identity_id = %identity_id,
267 requested_by = %idp_domain,
268 registrar = %identity.registrar_id_tag,
269 owner = ?identity.owner_id_tag,
270 status = ?identity.status,
271 "Unauthorized access to identity"
272 );
273 return Err(Error::PermissionDenied);
274 }
275
276 let response_data = IdentityResponse::from(identity);
277 let mut response = ApiResponse::new(response_data);
278 if let Some(id) = req_id {
279 response = response.with_req_id(id);
280 }
281
282 Ok((StatusCode::OK, Json(response)))
283}
284
285#[axum::debug_handler]
287pub async fn list_identities(
288 State(app): State<App>,
289 tn_id: TnId,
290 IdTag(idp_domain): IdTag,
291 Query(query_params): Query<ListIdentitiesQuery>,
292 OptionalRequestId(req_id): OptionalRequestId,
293) -> ClResult<(StatusCode, Json<ApiResponse<Vec<IdentityResponse>>>)> {
294 info!(
295 idp_domain = %idp_domain,
296 "GET /api/idp/identities"
297 );
298
299 check_idp_enabled(&app, tn_id).await?;
301
302 let idp_adapter = app.idp_adapter.as_ref().ok_or(Error::ServiceUnavailable(
304 "Identity Provider not available on this instance".to_string(),
305 ))?;
306
307 let opts = ListIdentityOptions {
308 id_tag_domain: idp_domain.to_string(),
309 email: query_params.email.clone(),
310 registrar_id_tag: None,
311 owner_id_tag: query_params.owner_id_tag.clone(),
312 status: query_params.status.as_ref().and_then(|s| s.parse().ok()),
313 expires_after: None,
314 expired_only: false,
315 limit: query_params.limit,
316 offset: query_params.offset,
317 };
318
319 let identities = idp_adapter.list_identities(opts).await?;
320
321 let response_data: Vec<IdentityResponse> =
322 identities.into_iter().map(IdentityResponse::from).collect();
323
324 let total = response_data.len();
325 let offset = query_params.offset.unwrap_or(0) as usize;
326 let limit = query_params.limit.unwrap_or(20) as usize;
327 let mut response = ApiResponse::with_pagination(response_data, offset, limit, total);
328 if let Some(id) = req_id {
329 response = response.with_req_id(id);
330 }
331
332 Ok((StatusCode::OK, Json(response)))
333}
334
335#[axum::debug_handler]
337pub async fn create_identity(
338 State(app): State<App>,
339 tn_id: TnId,
340 IdTag(idp_domain): IdTag,
341 OptionalRequestId(req_id): OptionalRequestId,
342 Json(create_req): Json<CreateIdentityRequest>,
343) -> ClResult<(StatusCode, Json<ApiResponse<IdentityResponse>>)> {
344 info!(
345 identity_id = %create_req.id_tag,
346 idp_domain = %idp_domain,
347 email = ?create_req.email,
348 owner_id_tag = ?create_req.owner_id_tag,
349 "POST /api/idp/identities - Creating new identity"
350 );
351
352 check_idp_enabled(&app, tn_id).await?;
354
355 let idp_adapter = app.idp_adapter.as_ref().ok_or(Error::ServiceUnavailable(
357 "Identity Provider not available on this instance".to_string(),
358 ))?;
359
360 if create_req.id_tag.is_empty() {
362 return Err(Error::ValidationError("id_tag is required".to_string()));
363 }
364
365 if create_req.owner_id_tag.is_none() && create_req.email.as_ref().is_none_or(|e| e.is_empty()) {
367 return Err(Error::ValidationError(
368 "email is required when no owner_id_tag is provided".to_string(),
369 ));
370 }
371
372 let (id_tag_prefix, id_tag_domain) =
374 parse_and_validate_identity_id_tag(&create_req.id_tag, &idp_domain)?;
375
376 if id_tag_prefix == "cl-o" {
378 warn!(
379 id_tag_prefix = %id_tag_prefix,
380 idp_domain = %idp_domain,
381 "Attempted to create identity with forbidden prefix 'cl-o'"
382 );
383 return Err(Error::ValidationError(
384 "Identity prefix 'cl-o' is reserved and cannot be used".to_string(),
385 ));
386 }
387
388 let renewal_interval_days = match app.settings.get(tn_id, "idp.renewal_interval").await {
393 Ok(SettingValue::Int(days)) => days,
394 Ok(_) => {
395 warn!(tn_id = tn_id.0, "Invalid idp.renewal_interval setting value");
396 return Err(Error::ConfigError(
397 "Invalid idp.renewal_interval setting value (expected integer days)".into(),
398 ));
399 }
400 Err(e) => {
401 warn!(tn_id = tn_id.0, error = ?e, "Failed to get idp.renewal_interval setting");
402 return Err(e);
403 }
404 };
405
406 let renewal_interval_seconds = renewal_interval_days * 24 * 60 * 60;
407 let expires_at = Timestamp::now().add_seconds(renewal_interval_seconds);
408
409 let address_type = if let Some(addr) = &create_req.address {
411 info!(
412 id_tag_prefix = %id_tag_prefix,
413 id_tag_domain = %id_tag_domain,
414 address = %addr,
415 "Creating identity with address"
416 );
417
418 match parse_address_type(addr) {
420 Ok(addr_type) => {
421 info!(
422 address = %addr,
423 address_type = ?addr_type,
424 "Parsed address type"
425 );
426 Some(addr_type)
427 }
428 Err(e) => {
429 warn!(
430 address = %addr,
431 error = ?e,
432 "Failed to parse address type"
433 );
434 None
435 }
436 }
437 } else {
438 info!(
439 id_tag_prefix = %id_tag_prefix,
440 id_tag_domain = %id_tag_domain,
441 "Creating identity without address"
442 );
443 None
444 };
445
446 let initial_status = if create_req.send_activation_email {
449 IdentityStatus::Pending } else {
451 IdentityStatus::Active };
453
454 let opts = CreateIdentityOptions {
455 id_tag_prefix: &id_tag_prefix,
456 id_tag_domain: &id_tag_domain,
457 email: create_req.email.as_deref(),
458 registrar_id_tag: &idp_domain,
459 owner_id_tag: create_req.owner_id_tag.as_deref(),
460 status: initial_status,
461 address: create_req.address.as_deref(),
462 address_type,
463 dyndns: create_req.dyndns,
464 lang: None,
465 expires_at: Some(expires_at),
466 };
467
468 info!(
469 id_tag_prefix = %id_tag_prefix,
470 id_tag_domain = %id_tag_domain,
471 "Calling IDP adapter create_identity"
472 );
473
474 let identity = idp_adapter.create_identity(opts).await.map_err(|e| {
475 warn!("Failed to create identity: {}", e);
476 e
477 })?;
478
479 info!(
480 id_tag_prefix = %identity.id_tag_prefix,
481 id_tag_domain = %identity.id_tag_domain,
482 address = ?identity.address,
483 "Identity created successfully"
484 );
485
486 let created_key = if create_req.create_api_key {
488 let key_name = create_req.api_key_name.as_deref().unwrap_or("identity-key");
489 let create_key_opts = cloudillo_types::identity_provider_adapter::CreateApiKeyOptions {
490 id_tag_prefix: &id_tag_prefix,
491 id_tag_domain: &id_tag_domain,
492 name: Some(key_name),
493 expires_at: None, };
495
496 match idp_adapter.create_api_key(create_key_opts).await {
497 Ok(key) => {
498 info!(
499 id_tag_prefix = %id_tag_prefix,
500 id_tag_domain = %id_tag_domain,
501 key_prefix = %key.api_key.key_prefix,
502 "API key created for identity"
503 );
504 Some(key.plaintext_key)
505 }
506 Err(e) => {
507 warn!("Failed to create API key for identity: {}", e);
508 None
509 }
510 }
511 } else {
512 None
513 };
514
515 if create_req.send_activation_email {
517 if let Some(ref email) = identity.email {
518 if let Err(e) = crate::registration::send_activation_email(
519 &app,
520 tn_id,
521 crate::registration::SendActivationEmailParams {
522 id_tag_prefix: &identity.id_tag_prefix,
523 id_tag_domain: &identity.id_tag_domain,
524 email,
525 lang: None,
526 },
527 )
528 .await
529 {
530 warn!(
531 id_tag_prefix = %id_tag_prefix,
532 id_tag_domain = %id_tag_domain,
533 error = %e,
534 "Failed to send activation email"
535 );
536 }
537 }
538 }
539
540 let mut response_data = IdentityResponse::from(identity);
541 response_data.api_key = created_key;
543
544 let mut response = ApiResponse::new(response_data);
545 if let Some(id) = req_id {
546 response = response.with_req_id(id);
547 }
548
549 Ok((StatusCode::CREATED, Json(response)))
550}
551
552#[allow(clippy::too_many_arguments)]
558#[axum::debug_handler]
559pub async fn update_identity_address(
560 State(app): State<App>,
561 tn_id: TnId,
562 IdTag(idp_domain): IdTag,
563 Auth(auth): Auth,
564 Path(identity_id): Path<String>,
565 ConnectInfo(socket_addr): ConnectInfo<SocketAddr>,
566 OptionalRequestId(req_id): OptionalRequestId,
567 body: Bytes,
568) -> ClResult<(StatusCode, Json<ApiResponse<AddressUpdateResponse>>)> {
569 info!(
570 identity_id = %identity_id,
571 idp_domain = %idp_domain,
572 auth_id_tag = %auth.id_tag,
573 "PUT /api/idp/identities/:id/address - Updating identity address"
574 );
575
576 let update_req: UpdateAddressRequest = if body.is_empty() {
578 UpdateAddressRequest::default()
579 } else {
580 serde_json::from_slice(&body)
581 .map_err(|e| Error::ValidationError(format!("Invalid JSON body: {}", e)))?
582 };
583
584 check_idp_enabled(&app, tn_id).await?;
586
587 let idp_adapter = app.idp_adapter.as_ref().ok_or(Error::ServiceUnavailable(
589 "Identity Provider not available on this instance".to_string(),
590 ))?;
591
592 let normalized_id = normalize_identity_path(&identity_id, &idp_domain);
594
595 let (id_tag_prefix, id_tag_domain) =
597 parse_and_validate_identity_id_tag(&normalized_id, &idp_domain)?;
598
599 let target_id_tag = format!("{}.{}", id_tag_prefix, id_tag_domain);
601
602 if auth.id_tag.as_ref() != target_id_tag {
604 warn!(
605 identity_id = %identity_id,
606 target_id_tag = %target_id_tag,
607 auth_id_tag = %auth.id_tag,
608 "Unauthorized update to identity address - identity mismatch"
609 );
610 return Err(Error::PermissionDenied);
611 }
612
613 let existing = idp_adapter
615 .read_identity(&id_tag_prefix, &id_tag_domain)
616 .await?
617 .ok_or(Error::NotFound)?;
618
619 let address_to_update = if update_req.auto_address {
623 socket_addr.ip().to_string()
625 } else {
626 match update_req.address {
627 Some(addr) if !addr.is_empty() && addr != "auto" => {
628 addr
630 }
631 _ => {
632 socket_addr.ip().to_string()
634 }
635 }
636 };
637
638 if let Some(current_addr) = &existing.address {
640 if current_addr.as_ref() == address_to_update {
641 info!(
643 identity_id = %identity_id,
644 address = %address_to_update,
645 "Address unchanged, skipping update"
646 );
647 let response_data = AddressUpdateResponse { address: address_to_update };
648 let mut response = ApiResponse::new(response_data);
649 if let Some(id) = req_id {
650 response = response.with_req_id(id);
651 }
652 return Ok((StatusCode::OK, Json(response)));
653 }
654 }
655
656 let address_type = parse_address_type(&address_to_update)?;
658
659 info!(
660 identity_id = %identity_id,
661 address = %address_to_update,
662 address_type = %address_type,
663 "Address validated and parsed"
664 );
665
666 let _updated_identity = idp_adapter
668 .update_identity_address(&id_tag_prefix, &id_tag_domain, &address_to_update, address_type)
669 .await
670 .map_err(|e| {
671 warn!("Failed to update identity address: {}", e);
672 e
673 })?;
674
675 let response_data = AddressUpdateResponse { address: address_to_update };
677 let mut response = ApiResponse::new(response_data);
678 if let Some(id) = req_id {
679 response = response.with_req_id(id);
680 }
681
682 Ok((StatusCode::OK, Json(response)))
683}
684
685#[axum::debug_handler]
687pub async fn delete_identity(
688 State(app): State<App>,
689 tn_id: TnId,
690 IdTag(idp_domain): IdTag,
691 Path(identity_id): Path<String>,
692 OptionalRequestId(req_id): OptionalRequestId,
693) -> ClResult<(StatusCode, Json<ApiResponse<()>>)> {
694 info!(
695 identity_id = %identity_id,
696 idp_domain = %idp_domain,
697 "DELETE /api/idp/identities/:id - Deleting identity"
698 );
699
700 check_idp_enabled(&app, tn_id).await?;
702
703 let idp_adapter = app.idp_adapter.as_ref().ok_or(Error::ServiceUnavailable(
705 "Identity Provider not available on this instance".to_string(),
706 ))?;
707
708 let (id_tag_prefix, id_tag_domain) =
710 parse_and_validate_identity_id_tag(&identity_id, &idp_domain)?;
711
712 let existing = idp_adapter
714 .read_identity(&id_tag_prefix, &id_tag_domain)
715 .await?
716 .ok_or(Error::NotFound)?;
717
718 if !can_access_identity(&existing, &idp_domain) {
720 warn!(
721 identity_id = %identity_id,
722 requested_by = %idp_domain,
723 registrar = %existing.registrar_id_tag,
724 owner = ?existing.owner_id_tag,
725 status = ?existing.status,
726 "Unauthorized deletion of identity"
727 );
728 return Err(Error::PermissionDenied);
729 }
730
731 idp_adapter.delete_identity(&id_tag_prefix, &id_tag_domain).await.map_err(|e| {
733 warn!("Failed to delete identity: {}", e);
734 e
735 })?;
736
737 let mut response = ApiResponse::new(());
738 if let Some(id) = req_id {
739 response = response.with_req_id(id);
740 }
741
742 Ok((StatusCode::OK, Json(response)))
743}
744
745#[derive(Debug, Deserialize)]
747#[serde(rename_all = "camelCase")]
748pub struct UpdateIdentitySettingsRequest {
749 pub dyndns: Option<bool>,
751}
752
753#[axum::debug_handler]
755pub async fn update_identity_settings(
756 State(app): State<App>,
757 tn_id: TnId,
758 IdTag(idp_domain): IdTag,
759 Path(identity_id): Path<String>,
760 OptionalRequestId(req_id): OptionalRequestId,
761 Json(update_req): Json<UpdateIdentitySettingsRequest>,
762) -> ClResult<(StatusCode, Json<ApiResponse<IdentityResponse>>)> {
763 info!(
764 identity_id = %identity_id,
765 idp_domain = %idp_domain,
766 dyndns = ?update_req.dyndns,
767 "PATCH /api/idp/identities/:id - Updating identity settings"
768 );
769
770 check_idp_enabled(&app, tn_id).await?;
772
773 let idp_adapter = app.idp_adapter.as_ref().ok_or(Error::ServiceUnavailable(
775 "Identity Provider not available on this instance".to_string(),
776 ))?;
777
778 let (id_tag_prefix, id_tag_domain) =
780 parse_and_validate_identity_id_tag(&identity_id, &idp_domain)?;
781
782 let existing = idp_adapter
784 .read_identity(&id_tag_prefix, &id_tag_domain)
785 .await?
786 .ok_or(Error::NotFound)?;
787
788 if !can_access_identity(&existing, &idp_domain) {
790 warn!(
791 identity_id = %identity_id,
792 requested_by = %idp_domain,
793 registrar = %existing.registrar_id_tag,
794 owner = ?existing.owner_id_tag,
795 status = ?existing.status,
796 "Unauthorized update to identity settings"
797 );
798 return Err(Error::PermissionDenied);
799 }
800
801 let update_opts = UpdateIdentityOptions { dyndns: update_req.dyndns, ..Default::default() };
803
804 let updated_identity = idp_adapter
806 .update_identity(&id_tag_prefix, &id_tag_domain, update_opts)
807 .await
808 .map_err(|e| {
809 warn!("Failed to update identity settings: {}", e);
810 e
811 })?;
812
813 let response_data = IdentityResponse::from(updated_identity);
814 let mut response = ApiResponse::new(response_data);
815 if let Some(id) = req_id {
816 response = response.with_req_id(id);
817 }
818
819 Ok((StatusCode::OK, Json(response)))
820}
821
822#[derive(Debug, Clone, Serialize, Deserialize)]
825pub struct IdpInfoResponse {
826 pub domain: String,
828 pub name: String,
830 pub info: String,
832 #[serde(skip_serializing_if = "Option::is_none")]
834 pub url: Option<String>,
835}
836
837#[axum::debug_handler]
843pub async fn get_idp_info(
844 State(app): State<App>,
845 tn_id: TnId,
846 OptionalRequestId(req_id): OptionalRequestId,
847) -> ClResult<(StatusCode, Json<ApiResponse<IdpInfoResponse>>)> {
848 info!(tn_id = tn_id.0, "GET /api/idp/info");
849
850 check_idp_enabled(&app, tn_id).await?;
852
853 let domain = app.auth_adapter.read_id_tag(tn_id).await?.to_string();
855
856 let name = match app.settings.get(tn_id, "idp.name").await {
858 Ok(SettingValue::String(s)) if !s.is_empty() => s,
859 _ => domain.clone(), };
861
862 let info = match app.settings.get(tn_id, "idp.info").await {
864 Ok(SettingValue::String(s)) => s,
865 _ => String::new(),
866 };
867
868 let url = match app.settings.get(tn_id, "idp.url").await {
870 Ok(SettingValue::String(s)) if !s.is_empty() => Some(s),
871 _ => None,
872 };
873
874 let response_data = IdpInfoResponse { domain, name, info, url };
875
876 let mut response = ApiResponse::new(response_data);
877 if let Some(id) = req_id {
878 response = response.with_req_id(id);
879 }
880
881 Ok((StatusCode::OK, Json(response)))
882}
883
884#[derive(Debug, Clone, Serialize, Deserialize)]
886pub struct AvailabilityResponse {
887 pub available: bool,
888 pub id_tag: String,
889}
890
891#[derive(Debug, Deserialize)]
893#[serde(rename_all = "camelCase")]
894pub struct CheckAvailabilityQuery {
895 pub id_tag: String,
897}
898
899#[axum::debug_handler]
905pub async fn check_identity_availability(
906 State(app): State<App>,
907 tn_id: TnId,
908 IdTag(my_id_tag): IdTag,
909 Query(query): Query<CheckAvailabilityQuery>,
910 OptionalRequestId(req_id): OptionalRequestId,
911) -> ClResult<(StatusCode, Json<ApiResponse<AvailabilityResponse>>)> {
912 let id_tag = query.id_tag.trim().to_lowercase();
913
914 info!(
915 id_tag = %id_tag,
916 registrar_id_tag = %my_id_tag,
917 "GET /api/idp/check-availability - Checking identity availability"
918 );
919
920 check_idp_enabled(&app, tn_id).await?;
922
923 let idp_adapter = app.idp_adapter.as_ref().ok_or(Error::ServiceUnavailable(
925 "Identity Provider not available on this instance".to_string(),
926 ))?;
927
928 if !id_tag.contains('.') {
930 return Err(Error::ValidationError(
931 "Identity id_tag must be in format 'prefix.domain' (e.g., 'alice.cloudillo.net')"
932 .to_string(),
933 ));
934 }
935
936 if let Some(first_dot_pos) = id_tag.find('.') {
938 let id_tag_prefix = &id_tag[..first_dot_pos];
939 let id_tag_domain = &id_tag[first_dot_pos + 1..];
940
941 if id_tag_prefix.is_empty() {
943 return Err(Error::ValidationError(
944 "Identity prefix cannot be empty (id_tag must be in format 'prefix.domain')"
945 .to_string(),
946 ));
947 }
948
949 if id_tag_prefix == "cl-o" {
951 warn!(
952 id_tag_prefix = %id_tag_prefix,
953 "Attempted to check availability for forbidden prefix 'cl-o'"
954 );
955 return Err(Error::ValidationError(
956 "Identity prefix 'cl-o' is reserved and cannot be used".to_string(),
957 ));
958 }
959
960 if id_tag_domain.is_empty() {
962 return Err(Error::ValidationError(
963 "Identity domain cannot be empty (id_tag must be in format 'prefix.domain')"
964 .to_string(),
965 ));
966 }
967
968 if id_tag_domain != my_id_tag.as_ref() {
970 warn!(
971 requested_domain = %id_tag_domain,
972 registrar_domain = %my_id_tag,
973 "Domain mismatch in availability check"
974 );
975 return Err(Error::PermissionDenied);
976 }
977
978 debug!(
979 id_tag = %id_tag,
980 prefix = %id_tag_prefix,
981 domain = %id_tag_domain,
982 "Parsed identity id_tag for availability check"
983 );
984
985 let identity_exists =
987 idp_adapter.read_identity(id_tag_prefix, id_tag_domain).await?.is_some();
988
989 let response_data =
990 AvailabilityResponse { available: !identity_exists, id_tag: id_tag.clone() };
991
992 let mut response = ApiResponse::new(response_data);
993 if let Some(id) = req_id {
994 response = response.with_req_id(id);
995 }
996
997 Ok((StatusCode::OK, Json(response)))
998 } else {
999 Err(Error::ValidationError(
1000 "Identity id_tag must contain at least one dot separator".to_string(),
1001 ))
1002 }
1003}
1004
1005#[derive(Debug, Deserialize)]
1007#[serde(rename_all = "camelCase")]
1008pub struct ActivateIdentityRequest {
1009 pub ref_id: String,
1011}
1012
1013#[axum::debug_handler]
1020pub async fn activate_identity(
1021 State(app): State<App>,
1022 tn_id: TnId,
1023 OptionalRequestId(req_id): OptionalRequestId,
1024 Json(activate_req): Json<ActivateIdentityRequest>,
1025) -> ClResult<(StatusCode, Json<ApiResponse<IdentityResponse>>)> {
1026 info!(
1027 ref_id = %activate_req.ref_id,
1028 "POST /api/idp/activate - Activating identity"
1029 );
1030
1031 check_idp_enabled(&app, tn_id).await?;
1033
1034 let idp_adapter = app.idp_adapter.as_ref().ok_or(Error::ServiceUnavailable(
1036 "Identity Provider not available on this instance".to_string(),
1037 ))?;
1038
1039 let (_ref_tn_id, _ref_id_tag, ref_data) = app
1041 .meta_adapter
1042 .use_ref(&activate_req.ref_id, &["idp.activation"])
1043 .await
1044 .map_err(|e| {
1045 warn!(ref_id = %activate_req.ref_id, error = ?e, "Invalid activation ref");
1046 e
1047 })?;
1048
1049 let identity_id = ref_data
1051 .resource_id
1052 .ok_or_else(|| Error::Internal("Activation ref missing resource_id".to_string()))?
1053 .to_string();
1054
1055 let (id_tag_prefix, id_tag_domain) = if let Some(dot_pos) = identity_id.find('.') {
1057 (identity_id[..dot_pos].to_string(), identity_id[dot_pos + 1..].to_string())
1058 } else {
1059 return Err(Error::ValidationError("Invalid identity id_tag format".to_string()));
1060 };
1061
1062 let existing = idp_adapter
1064 .read_identity(&id_tag_prefix, &id_tag_domain)
1065 .await?
1066 .ok_or(Error::NotFound)?;
1067
1068 if existing.status != IdentityStatus::Pending {
1070 warn!(
1071 identity_id = %identity_id,
1072 status = ?existing.status,
1073 "Cannot activate identity - not in Pending status"
1074 );
1075 return Err(Error::ValidationError(format!(
1076 "Identity is not in Pending status (current: {})",
1077 existing.status
1078 )));
1079 }
1080
1081 let update_opts =
1083 UpdateIdentityOptions { status: Some(IdentityStatus::Active), ..Default::default() };
1084
1085 let updated_identity = idp_adapter
1086 .update_identity(&id_tag_prefix, &id_tag_domain, update_opts)
1087 .await
1088 .map_err(|e| {
1089 warn!(identity_id = %identity_id, error = ?e, "Failed to activate identity");
1090 e
1091 })?;
1092
1093 info!(
1094 identity_id = %identity_id,
1095 registrar = %updated_identity.registrar_id_tag,
1096 owner = ?updated_identity.owner_id_tag,
1097 "Identity activated successfully - registrar access revoked"
1098 );
1099
1100 let response_data = IdentityResponse::from(updated_identity);
1101 let mut response = ApiResponse::new(response_data);
1102 if let Some(id) = req_id {
1103 response = response.with_req_id(id);
1104 }
1105
1106 Ok((StatusCode::OK, Json(response)))
1107}
1108
1109