Skip to main content

cloudillo_admin/
invite.rs

1//! Admin invite endpoint for community profile creation
2
3use axum::{extract::State, http::StatusCode, Json};
4use serde::{Deserialize, Serialize};
5use serde_with::skip_serializing_none;
6
7use crate::prelude::*;
8use cloudillo_core::extract::Auth;
9use cloudillo_core::CreateActionFn;
10use cloudillo_ref::service::{create_ref_internal, CreateRefInternalParams};
11use cloudillo_types::action_types::CreateAction;
12use cloudillo_types::types::{ApiResponse, Timestamp};
13
14/// Request body for creating a community invite
15#[derive(Debug, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct InviteCommunityRequest {
18	/// Target user to invite (must be connected)
19	pub target_id_tag: String,
20	/// Expiration in days (default: 30)
21	pub expires_in_days: Option<u32>,
22	/// Optional personal message
23	pub message: Option<String>,
24}
25
26/// Response for community invite creation
27#[skip_serializing_none]
28#[derive(Debug, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub struct InviteCommunityResponse {
31	pub ref_id: String,
32	pub invite_url: String,
33	pub target_id_tag: String,
34	pub expires_at: Option<i64>,
35}
36
37/// POST /api/admin/invite-community - Create a community invite and send PRINVT action
38pub async fn post_invite_community(
39	State(app): State<App>,
40	Auth(auth): Auth,
41	Json(req): Json<InviteCommunityRequest>,
42) -> ClResult<(StatusCode, Json<ApiResponse<InviteCommunityResponse>>)> {
43	let admin_tn_id = auth.tn_id;
44	let admin_id_tag = &auth.id_tag;
45	let expires_in_days = req.expires_in_days.unwrap_or(30);
46
47	info!(
48		admin = %admin_id_tag,
49		target = %req.target_id_tag,
50		"Creating community invite"
51	);
52
53	// Read admin tenant to get the id_tag for URL construction
54	let admin_tenant = app.meta_adapter.read_tenant(admin_tn_id).await?;
55	let node_id_tag = admin_tenant.id_tag.to_string();
56
57	// Calculate expiration
58	let expires_at = Some(Timestamp::now().add_seconds(expires_in_days as i64 * 86400));
59
60	// 1. Create single-use ref with type "profile.invite"
61	let (ref_id, invite_url) = create_ref_internal(
62		&app,
63		admin_tn_id,
64		CreateRefInternalParams {
65			id_tag: &node_id_tag,
66			typ: "profile.invite",
67			description: Some("Community profile invite"),
68			expires_at,
69			path_prefix: "/profile/new?invite=",
70			resource_id: None,
71			count: None, // Single use (default: 1)
72		},
73	)
74	.await?;
75
76	// 2. Create PRINVT action to deliver the invite to the target user
77	let prinvt_content = serde_json::json!({
78		"refId": ref_id,
79		"nodeName": admin_tenant.name.as_ref(),
80		"message": req.message,
81	});
82
83	let create_action_fn = app.ext::<CreateActionFn>()?;
84	if let Err(e) = create_action_fn(
85		&app,
86		admin_tn_id,
87		admin_id_tag,
88		CreateAction {
89			typ: "PRINVT".into(),
90			audience_tag: Some(req.target_id_tag.clone().into()),
91			content: Some(prinvt_content),
92			expires_at,
93			..Default::default()
94		},
95	)
96	.await
97	{
98		warn!(
99			error = %e,
100			target = %req.target_id_tag,
101			"Failed to send PRINVT action, invite ref was still created"
102		);
103	}
104
105	let response = InviteCommunityResponse {
106		ref_id,
107		invite_url,
108		target_id_tag: req.target_id_tag,
109		expires_at: expires_at.map(|ts| ts.0),
110	};
111
112	Ok((StatusCode::CREATED, Json(ApiResponse::new(response))))
113}
114
115// vim: ts=4