use anyhow::Result;
use clap::Args;
use serde_json::json;
use turnkey_client::generated::{
external::data::v1::{ApiKey, Timestamp},
immutable::{
activity::v1::{ApiKeyParamsV2, CreateApiKeysIntentV2},
common::v1::ApiKeyCurve,
},
services::coordinator::public::v1::{GetApiKeysRequest, GetApiKeysResponse},
};
use uuid::Uuid;
use super::duration::{ExpiresIn, human_seconds};
use super::public_key::CompressedPublicKey;
use crate::auth::ResolvedAuth;
use crate::errors::{ActivityError, ActivityErrorKind};
use crate::operations::{OperationOutput, query, submit_activity, unix_now};
const COMMAND: &str = "session.provision";
#[derive(Debug, Args)]
pub struct ProvisionArgs {
#[arg(long)]
user_id: Uuid,
#[arg(long)]
public_key: CompressedPublicKey,
#[arg(long, default_value = "7d")]
expires_in: ExpiresIn,
#[arg(long)]
label: Option<String>,
}
pub(super) async fn run(auth: ResolvedAuth, args: ProvisionArgs) -> Result<OperationOutput> {
let ProvisionArgs {
user_id,
public_key,
expires_in,
label,
} = args;
let existing: GetApiKeysResponse = query(
"/public/v1/query/get_api_keys",
&GetApiKeysRequest {
organization_id: auth.org_id.to_string(),
user_id: Some(user_id.to_string()),
},
&auth,
)
.await?;
if let Some(key) = existing
.api_keys
.into_iter()
.find(|key| public_key.matches(key))
{
let ApiKey {
credential: _,
api_key_id,
api_key_name,
created_at,
updated_at: _,
expiration_seconds,
} = key;
return Ok(OperationOutput::result(
COMMAND,
json!({
"userId": user_id,
"expiresIn": expiration_seconds.map(human_seconds),
"expirationSeconds": expiration_seconds.map(|seconds| seconds.to_string()),
"publicKey": public_key,
"apiKeyId": api_key_id,
"apiKeyName": api_key_name,
"createdAt": created_at.map(|Timestamp { seconds, nanos: _ }| seconds),
"alreadyRegistered": true,
}),
));
}
let api_key_name = match label {
Some(label) => label,
None => format!("session-{expires_in}-{}", unix_now()?.as_secs()),
};
let submitted = submit_activity(
&auth,
COMMAND,
"create_api_keys",
"ACTIVITY_TYPE_CREATE_API_KEYS_V2",
&CreateApiKeysIntentV2 {
api_keys: vec![ApiKeyParamsV2 {
api_key_name: api_key_name.clone(),
public_key: public_key.to_string(),
curve_type: ApiKeyCurve::P256,
expiration_seconds: Some(expires_in.seconds().to_string()),
}],
user_id: user_id.to_string(),
},
)
.await?;
let activity = &submitted.data()["activity"];
let (api_key_id, next_step) = match submitted.pending_activity_id() {
Some(activity_id) => (
None,
Some(format!(
"approve activity {activity_id} (expiring key for user {user_id}, lifetime {expires_in}), then re-run this command or tk activity wait {activity_id}"
)),
),
None => {
let api_key_id = activity["result"]["createApiKeysResult"]["apiKeyIds"][0]
.as_str()
.ok_or_else(|| {
ActivityError::new(
ActivityErrorKind::MalformedResponse,
"create_api_keys completed without an apiKeyId",
)
})?;
(Some(api_key_id), None)
}
};
let mut data = json!({
"userId": user_id,
"expiresIn": expires_in.to_string(),
"expirationSeconds": expires_in.seconds().to_string(),
"publicKey": public_key,
"apiKeyName": api_key_name,
"apiKeyId": api_key_id,
"activity": {
"id": activity["id"],
"status": activity["status"],
"type": activity["type"],
},
});
if let Some(step) = next_step {
data["nextStep"] = step.into();
}
Ok(OperationOutput::result(COMMAND, data))
}