use miette::IntoDiagnostic;
use std::time::{Duration, Instant};
use tracing::{debug, error, info, warn};
use ockam_api::cli_state;
use ockam_api::cloud::project::{Project, ProjectsOrchestratorApi};
use ockam_api::cloud::space::{Space, Spaces};
use ockam_api::enroll::enrollment::Enrollment;
use ockam_api::enroll::oidc_service::OidcService;
use crate::api::notification::rust::{Kind, Notification};
use crate::api::state::OrchestratorStatus;
use crate::state::{AppState, NODE_NAME, PROJECT_NAME};
use crate::Result;
enum EnrollmentOutcome {
AlreadyEnrolled,
PendingValidation,
Successful,
}
impl AppState {
pub async fn enroll_user(&self) -> Result<()> {
let result = self.enroll_with_token().await;
match result {
Ok(outcome) => match outcome {
EnrollmentOutcome::AlreadyEnrolled => {
return Ok(());
}
EnrollmentOutcome::PendingValidation => {
self.update_orchestrator_status(OrchestratorStatus::Disconnected);
self.publish_state().await;
return Ok(());
}
EnrollmentOutcome::Successful => {
self.notify(Notification {
kind: Kind::Information,
title: "Enrolled successfully".to_string(),
message: "You're ready to create your first portal.".to_string(),
});
}
},
Err(err) => {
error!(?err, "Failed to enroll");
self.update_orchestrator_status(OrchestratorStatus::Disconnected);
self.publish_state().await;
self.notify(Notification {
kind: Kind::Error,
title: "Failed to enroll".to_string(),
message: format!("{}", err),
});
return Err(err);
}
}
self.reset_node_manager().await?;
self.schedule_relay_refresh_now();
self.schedule_invitations_refresh_now();
self.schedule_projects_refresh_now();
info!("User enrolled successfully");
Ok(())
}
async fn enroll_with_token(&self) -> Result<EnrollmentOutcome> {
if self.is_enrolled().await.unwrap_or_default() {
debug!("User is already enrolled");
return Ok(EnrollmentOutcome::AlreadyEnrolled);
}
self.update_orchestrator_status(OrchestratorStatus::WaitingForToken);
self.publish_state().await;
let oidc_service = OidcService::new()?;
let token = oidc_service.get_token_with_pkce().await?;
let mut user_info = oidc_service.get_user_info(&token).await?;
info!(?user_info, "User info retrieved successfully");
if !user_info.email_verified {
self.update_orchestrator_status(OrchestratorStatus::WaitingForEmailValidation);
self.publish_state().await;
let timeout_timestamp = Instant::now() + Duration::from_secs(60 * 10);
while !user_info.email_verified {
if Instant::now() > timeout_timestamp {
warn!("Timeout waiting for email validation");
return Ok(EnrollmentOutcome::PendingValidation);
}
tokio::time::sleep(Duration::from_secs(10)).await;
user_info = oidc_service.get_user_info(&token).await?;
}
}
let cli_state = self.state().await;
cli_state.store_user(&user_info).await?;
cli_state.set_default_user(&user_info.email).await?;
{
let controller = self.controller().await.into_diagnostic()?;
controller
.enroll_with_oidc_token(&self.context(), token)
.await?;
}
self.update_orchestrator_status(OrchestratorStatus::RetrievingSpace);
self.publish_state().await;
let space = self.retrieve_space().await?;
self.update_orchestrator_status(OrchestratorStatus::RetrievingProject);
self.publish_state().await;
self.retrieve_project(&space).await?;
let cli_state = self.state().await;
let node = cli_state.get_node(NODE_NAME).await?;
let identifier = node.identifier();
cli_state
.set_identifier_as_enrolled(&identifier, &user_info.email)
.await
.into_diagnostic()?;
info!(%identifier, "User enrolled successfully");
Ok(EnrollmentOutcome::Successful)
}
async fn retrieve_space(&self) -> Result<Space> {
info!("retrieving the user's space");
let node_manager = self.node_manager().await;
let context = self.context();
let spaces = {
let mut spaces = node_manager.get_spaces(&context).await?;
spaces.sort_by(|s1, s2| s1.name.cmp(&s2.name));
spaces
};
let space = match spaces.first() {
Some(space) => space.clone(),
None => {
let space_name = cli_state::random_name();
node_manager
.create_space(&self.context(), &space_name, vec![])
.await?
}
};
Ok(space)
}
async fn retrieve_project(&self, space: &Space) -> Result<Project> {
info!("retrieving the user project");
let node_manager = self.node_manager().await;
let projects = node_manager.get_admin_projects(&self.context()).await?;
let main_project = projects.iter().find(|p| p.name() == PROJECT_NAME);
let project = match main_project {
Some(project) => project.clone(),
None => {
self.notify(Notification {
kind: Kind::Information,
title: "Provisioning a project".to_string(),
message:
"We're provisioning a dedicated project for you in Ockam Orchestrator. \
This can take up to 3 minutes."
.to_string(),
});
let ctx = &self.context();
let project = node_manager
.create_project(ctx, &space.name, PROJECT_NAME, vec![])
.await?;
let project = node_manager
.wait_until_project_creation_operation_is_complete(ctx, project)
.await?;
node_manager
.wait_until_project_is_ready(ctx, project)
.await?
}
};
self.state()
.await
.projects()
.set_default_project(project.project_id())
.await?;
self.state()
.await
.set_node_project(&node_manager.node_name(), &Some(project.name().to_string()))
.await?;
Ok(project)
}
}