Skip to content

Commit dd68846

Browse files
committed
Add invitation email delivery admin endpoints
1 parent 837c4a5 commit dd68846

11 files changed

Lines changed: 1210 additions & 66 deletions

File tree

crates/api/src/conversions.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,44 @@ pub fn services_invitation_email_status_to_api(
648648
}
649649
}
650650

651+
pub fn api_invitation_status_to_services(
652+
status: crate::models::InvitationStatus,
653+
) -> services::organization::InvitationStatus {
654+
match status {
655+
crate::models::InvitationStatus::Pending => {
656+
services::organization::InvitationStatus::Pending
657+
}
658+
crate::models::InvitationStatus::Accepted => {
659+
services::organization::InvitationStatus::Accepted
660+
}
661+
crate::models::InvitationStatus::Declined => {
662+
services::organization::InvitationStatus::Declined
663+
}
664+
crate::models::InvitationStatus::Expired => {
665+
services::organization::InvitationStatus::Expired
666+
}
667+
}
668+
}
669+
670+
pub fn api_invitation_email_status_to_services(
671+
status: crate::models::InvitationEmailStatus,
672+
) -> services::organization::InvitationEmailStatus {
673+
match status {
674+
crate::models::InvitationEmailStatus::NotAttempted => {
675+
services::organization::InvitationEmailStatus::NotAttempted
676+
}
677+
crate::models::InvitationEmailStatus::Sent => {
678+
services::organization::InvitationEmailStatus::Sent
679+
}
680+
crate::models::InvitationEmailStatus::Failed => {
681+
services::organization::InvitationEmailStatus::Failed
682+
}
683+
crate::models::InvitationEmailStatus::Skipped => {
684+
services::organization::InvitationEmailStatus::Skipped
685+
}
686+
}
687+
}
688+
651689
/// Convert services OrganizationInvitation to API OrganizationInvitationResponse
652690
pub fn services_invitation_to_api(
653691
invitation: services::organization::OrganizationInvitation,
@@ -680,6 +718,47 @@ pub fn services_invitation_to_api_with_org(
680718
}
681719
}
682720

721+
pub fn services_invitation_email_delivery_to_api(
722+
delivery: services::organization::OrganizationInvitationEmailDelivery,
723+
) -> crate::models::AdminInvitationEmailDeliveryResponse {
724+
let invitation = delivery.invitation;
725+
726+
crate::models::AdminInvitationEmailDeliveryResponse {
727+
organization_id: invitation.organization_id.0.to_string(),
728+
organization_name: delivery.organization_name,
729+
invitation_id: invitation.id.to_string(),
730+
recipient_email: invitation.email,
731+
role: services_role_to_api_role(invitation.role),
732+
invitation_status: services_invitation_status_to_api(invitation.status),
733+
email_status: services_invitation_email_status_to_api(invitation.email_status),
734+
email_sent_at: invitation.email_sent_at,
735+
email_last_error: invitation.email_last_error,
736+
email_message_id: invitation.email_message_id,
737+
invited_by_user_id: invitation.invited_by_user_id.0.to_string(),
738+
invited_by_email: delivery.invited_by_email,
739+
invited_by_display_name: delivery.invited_by_display_name,
740+
created_at: invitation.created_at,
741+
expires_at: invitation.expires_at,
742+
responded_at: invitation.responded_at,
743+
}
744+
}
745+
746+
pub fn services_invitation_resend_result_to_api(
747+
result: services::organization::InvitationEmailResendResult,
748+
) -> crate::models::AdminInvitationEmailResendResultResponse {
749+
crate::models::AdminInvitationEmailResendResultResponse {
750+
invitation_id: result.invitation_id.to_string(),
751+
recipient_email: result.recipient_email,
752+
success: result.success,
753+
email_sent: result.email_sent,
754+
email_status: services_invitation_email_status_to_api(result.email_status),
755+
email_sent_at: result.email_sent_at,
756+
email_message_id: result.email_message_id,
757+
email_last_error: result.email_last_error,
758+
error: result.error,
759+
}
760+
}
761+
683762
/// Convert database::User to AdminUserResponse (for owners/admins only)
684763
pub fn db_user_to_admin_user(user: &database::User) -> AdminUserResponse {
685764
AdminUserResponse {

crates/api/src/lib.rs

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -874,10 +874,13 @@ pub fn build_app_with_config(
874874
database.clone(),
875875
&auth_components.auth_state_middleware,
876876
config.clone(),
877-
app_state.inference_provider_pool.clone(),
878-
analytics_service,
879-
domain_services.models_service.clone(),
880-
domain_services.completion_service.clone(),
877+
AdminRouteDependencies {
878+
inference_provider_pool: app_state.inference_provider_pool.clone(),
879+
analytics_service,
880+
models_service: domain_services.models_service.clone(),
881+
completion_service: domain_services.completion_service.clone(),
882+
organization_service: domain_services.organization_service.clone(),
883+
},
881884
);
882885

883886
let invitation_routes =
@@ -1529,23 +1532,30 @@ fn cache_control_layer(value: &'static str) -> CacheControlLayer {
15291532
}
15301533

15311534
/// Build admin routes (authenticated endpoints)
1535+
pub struct AdminRouteDependencies {
1536+
pub inference_provider_pool: Arc<services::inference_provider_pool::InferenceProviderPool>,
1537+
pub analytics_service: Arc<services::admin::AnalyticsService>,
1538+
pub models_service: Arc<services::models::ModelsServiceImpl>,
1539+
pub completion_service: Arc<services::CompletionServiceImpl>,
1540+
pub organization_service:
1541+
Arc<dyn services::organization::OrganizationServiceTrait + Send + Sync>,
1542+
}
1543+
15321544
pub fn build_admin_routes(
15331545
database: Arc<Database>,
15341546
auth_state_middleware: &AuthState,
15351547
config: Arc<ApiConfig>,
1536-
inference_provider_pool: Arc<services::inference_provider_pool::InferenceProviderPool>,
1537-
analytics_service: Arc<services::admin::AnalyticsService>,
1538-
models_service: Arc<services::models::ModelsServiceImpl>,
1539-
completion_service: Arc<services::CompletionServiceImpl>,
1548+
dependencies: AdminRouteDependencies,
15401549
) -> Router {
15411550
use crate::middleware::admin_middleware;
15421551
use crate::routes::admin::{
15431552
batch_upsert_models, create_admin_access_token, create_service, delete_admin_access_token,
15441553
delete_model, deprecate_model, get_model_history, get_organization_concurrent_limit,
15451554
get_organization_limits_history, get_organization_metrics, get_organization_timeseries,
1546-
get_platform_metrics, list_admin_access_tokens, list_models as admin_list_models,
1547-
list_organizations, list_users, update_organization_concurrent_limit,
1548-
update_organization_limits, update_service, AdminAppState,
1555+
get_platform_metrics, list_admin_access_tokens, list_invitation_email_deliveries,
1556+
list_models as admin_list_models, list_organizations, list_users, resend_invitation_email,
1557+
update_organization_concurrent_limit, update_organization_limits, update_service,
1558+
AdminAppState,
15491559
};
15501560
use database::repositories::{AdminAccessTokenRepository, AdminCompositeRepository};
15511561
use services::admin::AdminServiceImpl;
@@ -1567,17 +1577,19 @@ pub fn build_admin_routes(
15671577
// `/v1/admin/organizations/{org_id}/concurrent-limit`.
15681578
let admin_service = Arc::new(AdminServiceImpl::new(
15691579
admin_repository as Arc<dyn services::admin::AdminRepository>,
1570-
models_service as Arc<dyn services::models::ModelsServiceTrait>,
1571-
completion_service.clone() as Arc<dyn services::completions::CompletionServiceTrait>,
1580+
dependencies.models_service as Arc<dyn services::models::ModelsServiceTrait>,
1581+
dependencies.completion_service.clone()
1582+
as Arc<dyn services::completions::CompletionServiceTrait>,
15721583
)) as Arc<dyn services::admin::AdminService + Send + Sync>;
15731584

15741585
let admin_app_state = AdminAppState {
15751586
admin_service,
1576-
analytics_service,
1587+
analytics_service: dependencies.analytics_service,
1588+
organization_service: dependencies.organization_service,
15771589
auth_service: auth_state_middleware.auth_service.clone(),
15781590
config,
15791591
admin_access_token_repository,
1580-
inference_provider_pool,
1592+
inference_provider_pool: dependencies.inference_provider_pool,
15811593
};
15821594

15831595
Router::new()
@@ -1624,6 +1636,14 @@ pub fn build_admin_routes(
16241636
"/admin/platform/metrics",
16251637
axum::routing::get(get_platform_metrics),
16261638
)
1639+
.route(
1640+
"/admin/invitation-email-deliveries",
1641+
axum::routing::get(list_invitation_email_deliveries),
1642+
)
1643+
.route(
1644+
"/admin/invitation-email-deliveries/{invitation_id}/resend",
1645+
axum::routing::post(resend_invitation_email),
1646+
)
16271647
.route("/admin/users", axum::routing::get(list_users))
16281648
.route(
16291649
"/admin/organizations",

crates/api/src/models.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2561,6 +2561,50 @@ pub struct OrganizationInvitationWithOrgResponse {
25612561
pub invited_by_display_name: Option<String>,
25622562
}
25632563

2564+
/// Admin view of invitation email delivery metadata.
2565+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
2566+
pub struct AdminInvitationEmailDeliveryResponse {
2567+
pub organization_id: String,
2568+
pub organization_name: String,
2569+
pub invitation_id: String,
2570+
pub recipient_email: String,
2571+
pub role: MemberRole,
2572+
pub invitation_status: InvitationStatus,
2573+
pub email_status: InvitationEmailStatus,
2574+
pub email_sent_at: Option<DateTime<Utc>>,
2575+
pub email_last_error: Option<String>,
2576+
pub email_message_id: Option<String>,
2577+
pub invited_by_user_id: String,
2578+
pub invited_by_email: Option<String>,
2579+
pub invited_by_display_name: Option<String>,
2580+
pub created_at: DateTime<Utc>,
2581+
pub expires_at: DateTime<Utc>,
2582+
pub responded_at: Option<DateTime<Utc>>,
2583+
}
2584+
2585+
/// Paginated admin invitation email delivery response.
2586+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
2587+
pub struct ListAdminInvitationEmailDeliveriesResponse {
2588+
pub deliveries: Vec<AdminInvitationEmailDeliveryResponse>,
2589+
pub total: i64,
2590+
pub limit: i64,
2591+
pub offset: i64,
2592+
}
2593+
2594+
/// Admin invitation email resend result.
2595+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
2596+
pub struct AdminInvitationEmailResendResultResponse {
2597+
pub invitation_id: String,
2598+
pub recipient_email: String,
2599+
pub success: bool,
2600+
pub email_sent: bool,
2601+
pub email_status: InvitationEmailStatus,
2602+
pub email_sent_at: Option<DateTime<Utc>>,
2603+
pub email_message_id: Option<String>,
2604+
pub email_last_error: Option<String>,
2605+
pub error: Option<String>,
2606+
}
2607+
25642608
/// Accept invitation response
25652609
#[derive(Debug, Serialize, Deserialize, ToSchema)]
25662610
pub struct AcceptInvitationResponse {

crates/api/src/openapi.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ use utoipa::{Modify, OpenApi};
143143
crate::routes::admin::get_organization_metrics,
144144
crate::routes::admin::get_platform_metrics,
145145
crate::routes::admin::get_organization_timeseries,
146+
crate::routes::admin::list_invitation_email_deliveries,
147+
crate::routes::admin::resend_invitation_email,
146148
crate::routes::admin::list_users,
147149
crate::routes::admin::create_admin_access_token,
148150
crate::routes::admin::list_admin_access_tokens,
@@ -232,6 +234,9 @@ use utoipa::{Modify, OpenApi};
232234
// Organization concurrent limit models (Admin)
233235
UpdateOrganizationConcurrentLimitRequest, UpdateOrganizationConcurrentLimitResponse,
234236
GetOrganizationConcurrentLimitResponse,
237+
// Invitation email delivery models (Admin)
238+
AdminInvitationEmailDeliveryResponse, ListAdminInvitationEmailDeliveriesResponse,
239+
AdminInvitationEmailResendResultResponse,
235240
// User models (Admin)
236241
ListUsersResponse, AdminUserResponse,
237242
// Admin access token models

0 commit comments

Comments
 (0)