1use axum::{
4 extract::{Path, State},
5 http::StatusCode,
6 Json,
7};
8
9use crate::prelude::*;
10use cloudillo_core::{
11 bootstrap_types::CreateCompleteTenantOptions, extract::Auth, CreateActionFn,
12 CreateCompleteTenantFn,
13};
14use cloudillo_idp::registration::{IdpRegContent, IdpRegResponse};
15use cloudillo_types::{
16 action_types::CreateAction,
17 meta_adapter::{
18 Profile, ProfileConnectionStatus, ProfileType, UpdateProfileData, UpdateTenantData,
19 },
20 types::{ApiResponse, CommunityProfileResponse, CreateCommunityRequest},
21 utils::derive_name_from_id_tag,
22};
23
24pub async fn put_community_profile(
26 State(app): State<App>,
27 Auth(auth): Auth,
28 Path(id_tag): Path<String>,
29 Json(req): Json<CreateCommunityRequest>,
30) -> ClResult<(StatusCode, Json<ApiResponse<CommunityProfileResponse>>)> {
31 let id_tag_lower = id_tag.to_lowercase();
32 let creator_id_tag = &auth.id_tag;
33 let creator_tn_id = auth.tn_id;
34
35 let is_admin = auth.roles.iter().any(|r| r.as_ref() == "SADM");
37 let invite_ref = req.invite_ref.as_deref();
38 if !is_admin {
39 let ref_code = invite_ref.ok_or_else(|| {
40 Error::ValidationError("Community creation requires an invite".into())
41 })?;
42 app.meta_adapter.validate_ref(ref_code, &["profile.invite"]).await?;
44 }
45
46 info!(
47 creator = %creator_id_tag,
48 community = %id_tag_lower,
49 typ = %req.typ,
50 "Creating community profile"
51 );
52
53 if req.typ != "idp" && req.typ != "domain" {
55 return Err(Error::ValidationError("Invalid identity type".into()));
56 }
57
58 let providers = crate::register::get_identity_providers(&app, TnId(1)).await;
60 let validation = crate::register::verify_register_data(
61 &app,
62 &req.typ,
63 &id_tag_lower,
64 req.app_domain.as_deref(),
65 providers,
66 )
67 .await?;
68
69 if !validation.id_tag_error.is_empty() {
70 warn!(
71 community = %id_tag_lower,
72 error = %validation.id_tag_error,
73 "Community id_tag validation failed"
74 );
75 return Err(Error::ValidationError(validation.id_tag_error));
76 }
77
78 let idp_api_key: Option<String> = if req.typ == "idp" {
80 let idp_domain = match id_tag_lower.find('.') {
82 Some(pos) => &id_tag_lower[pos + 1..],
83 None => return Err(Error::ValidationError("Invalid IDP id_tag format".into())),
84 };
85
86 let address = if app.opts.local_address.is_empty() {
88 None
89 } else {
90 Some(app.opts.local_address.iter().map(|s| s.as_ref()).collect::<Vec<_>>().join(","))
91 };
92
93 let reg_content = IdpRegContent {
94 id_tag: id_tag_lower.clone(),
95 email: None, owner_id_tag: Some(creator_id_tag.to_string()), issuer: None,
98 address,
99 lang: None, };
101
102 let action = CreateAction {
104 typ: "IDP:REG".into(),
105 sub_typ: None,
106 parent_id: None,
107 audience_tag: Some(idp_domain.to_string().into()),
108 content: Some(serde_json::to_value(®_content)?),
109 attachments: None,
110 subject: None,
111 expires_at: Some(Timestamp::now().add_seconds(86400 * 30)),
112 visibility: None,
113 flags: None,
114 x: None,
115 };
116
117 let action_token = app.auth_adapter.create_action_token(TnId(1), action).await?;
119
120 #[derive(serde::Serialize)]
121 struct InboxRequest {
122 token: String,
123 }
124
125 info!(
126 community = %id_tag_lower,
127 idp_domain = %idp_domain,
128 "Registering community with identity provider"
129 );
130
131 let idp_response: cloudillo_types::types::ApiResponse<serde_json::Value> = app
132 .request
133 .post_public(
134 idp_domain,
135 "/inbox/sync",
136 &InboxRequest { token: action_token.to_string() },
137 )
138 .await
139 .map_err(|e| {
140 warn!(error = %e, idp_domain = %idp_domain, "Failed to register community with IDP");
141 Error::ValidationError("IDP registration failed".into())
142 })?;
143
144 let idp_reg_result: IdpRegResponse = serde_json::from_value(idp_response.data)
146 .map_err(|e| Error::Internal(format!("IDP response parsing failed: {}", e)))?;
147
148 if !idp_reg_result.success {
149 warn!(
150 community = %id_tag_lower,
151 message = %idp_reg_result.message,
152 "IDP registration failed"
153 );
154 return Err(Error::ValidationError(idp_reg_result.message));
155 }
156
157 info!(
158 community = %id_tag_lower,
159 "Community registered with identity provider"
160 );
161
162 idp_reg_result.api_key
163 } else {
164 None
165 };
166
167 let display_name = req.name.clone().unwrap_or_else(|| derive_name_from_id_tag(&id_tag_lower));
169 let create_tenant = app.ext::<CreateCompleteTenantFn>()?;
170 let community_tn_id = create_tenant(
171 &app,
172 CreateCompleteTenantOptions {
173 id_tag: &id_tag_lower,
174 email: None,
175 password: None,
176 roles: None,
177 display_name: Some(&display_name),
178 create_acme_cert: app.opts.acme_email.is_some(),
179 acme_email: app.opts.acme_email.as_deref(),
180 app_domain: req.app_domain.as_deref(),
181 },
182 )
183 .await?;
184
185 info!(
186 community = %id_tag_lower,
187 tn_id = ?community_tn_id,
188 "Community tenant created"
189 );
190
191 if let Some(api_key) = &idp_api_key {
193 info!(
194 community = %id_tag_lower,
195 "Storing IDP API key for community"
196 );
197 if let Err(e) = app.auth_adapter.update_idp_api_key(&id_tag_lower, api_key).await {
198 warn!(error = %e, community = %id_tag_lower, "Failed to store IDP API key");
199 }
201 }
202
203 app.meta_adapter
206 .update_tenant(
207 community_tn_id,
208 &UpdateTenantData {
209 typ: Patch::Value(ProfileType::Community),
210 profile_pic: match &req.profile_pic {
211 Some(pic) => Patch::Value(pic.clone()),
212 None => Patch::Undefined,
213 },
214 ..Default::default()
215 },
216 )
217 .await?;
218
219 app.meta_adapter
221 .update_setting(
222 community_tn_id,
223 "federation.auto_approve",
224 Some(serde_json::Value::Bool(true)),
225 )
226 .await?;
227
228 info!(
230 creator = %creator_id_tag,
231 community = %id_tag_lower,
232 "Creating CONN action from creator to community"
233 );
234 let create_action = app.ext::<CreateActionFn>()?;
235 create_action(
236 &app,
237 creator_tn_id,
238 creator_id_tag,
239 CreateAction {
240 typ: "CONN".into(),
241 audience_tag: Some(id_tag_lower.clone().into()),
242 ..Default::default()
243 },
244 )
245 .await?;
246
247 info!(
250 creator = %creator_id_tag,
251 community = %id_tag_lower,
252 "Creating CONN action from community to creator"
253 );
254 let create_action = app.ext::<CreateActionFn>()?;
255 create_action(
256 &app,
257 community_tn_id,
258 &id_tag_lower,
259 CreateAction {
260 typ: "CONN".into(),
261 audience_tag: Some(creator_id_tag.to_string().into()),
262 ..Default::default()
263 },
264 )
265 .await?;
266
267 app.meta_adapter
271 .update_profile(
272 creator_tn_id,
273 &id_tag_lower,
274 &UpdateProfileData {
275 connected: Patch::Value(ProfileConnectionStatus::Connected),
276 ..Default::default()
277 },
278 )
279 .await?;
280
281 let creator_name = match app.meta_adapter.get_profile_info(creator_tn_id, creator_id_tag).await
283 {
284 Ok(profile) => profile.name.to_string(),
285 Err(_) => derive_name_from_id_tag(creator_id_tag),
286 };
287
288 let creator_profile = Profile {
290 id_tag: creator_id_tag.as_ref(),
291 name: creator_name.as_str(),
292 typ: ProfileType::Person,
293 profile_pic: None,
294 following: false,
295 connected: ProfileConnectionStatus::Connected,
296 roles: None,
297 };
298 app.meta_adapter.create_profile(community_tn_id, &creator_profile, "").await?;
299
300 app.meta_adapter
302 .update_profile(
303 community_tn_id,
304 creator_id_tag,
305 &UpdateProfileData {
306 roles: Patch::Value(Some(vec!["leader".to_string().into()])),
307 connected: Patch::Value(ProfileConnectionStatus::Connected),
308 following: Patch::Undefined,
309 ..Default::default()
310 },
311 )
312 .await?;
313
314 info!(
315 creator = %creator_id_tag,
316 community = %id_tag_lower,
317 "Creator assigned leader role in community"
318 );
319
320 if let Some(ref_code) = invite_ref {
322 if let Err(e) = app.meta_adapter.use_ref(ref_code, &["profile.invite"]).await {
323 warn!(error = %e, "Failed to consume invite ref after community creation");
324 }
325 }
326
327 let response = CommunityProfileResponse {
329 id_tag: id_tag_lower,
330 name: display_name,
331 r#type: "community".to_string(),
332 profile_pic: req.profile_pic,
333 created_at: Timestamp::now(),
334 };
335
336 Ok((StatusCode::CREATED, Json(ApiResponse::new(response))))
337}
338
339