use crate::client::upgrade::Tier;
use crate::server::{AppError, AppState, AuthUser};
use anyhow::anyhow;
use axum::{
extract::{Path, State},
Json,
};
use serde::{Deserialize, Serialize};
use vault::fact::MemoFact;
fn team_vault_key(team_id: &str, project_hash: &str) -> String {
format!("team:{team_id}:{project_hash}")
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TeamInfo {
pub team_id: String,
pub owner_id: String,
pub name: String,
pub member_count: usize,
pub tier: String,
}
#[derive(Deserialize)]
pub struct CreateTeamReq {
pub name: String,
}
#[derive(Serialize)]
pub struct CreateTeamResp {
pub team_id: String,
pub name: String,
}
#[derive(Deserialize)]
pub struct InviteMemberReq {
pub email: String,
}
#[derive(Serialize)]
pub struct InviteMemberResp {
pub ok: bool,
pub invited_user_id: Option<String>,
}
#[derive(Deserialize)]
pub struct TeamVaultPushReq {
pub project_hash: String,
pub facts: Vec<MemoFact>,
}
#[derive(Serialize)]
pub struct TeamVaultPushResp {
pub accepted: usize,
pub server_version: u64,
}
#[derive(Serialize)]
pub struct TeamVaultPullResp {
pub facts: Vec<MemoFact>,
pub server_version: u64,
}
pub async fn create_team(
State(state): State<AppState>,
AuthUser(user_id): AuthUser,
Json(req): Json<CreateTeamReq>,
) -> Result<Json<CreateTeamResp>, AppError> {
let user = state
.db
.get_user(&user_id)
.ok_or_else(|| AppError(anyhow!("user not found")))?;
let tier = Tier::parse_tier(&user.tier);
if !tier.team_vault_enabled() {
return Err(AppError(anyhow!(
"Team namespaces require Studio tier or higher. Upgrade at {}",
tier.upgrade_url()
)));
}
let team_id = uuid::Uuid::new_v4().to_string();
let meta = serde_json::json!({
"name": req.name,
"owner_id": user_id,
"members": [user_id],
});
state
.db
.upsert_vault(&user_id, &format!("team_meta:{team_id}"), &meta.to_string())
.map_err(|e| AppError(anyhow!(e)))?;
Ok(Json(CreateTeamResp {
team_id,
name: req.name,
}))
}
pub async fn get_team(
State(state): State<AppState>,
AuthUser(user_id): AuthUser,
Path(team_id): Path<String>,
) -> Result<Json<TeamInfo>, AppError> {
let meta_key = format!("team_meta:{team_id}");
let record = state
.db
.get_vault(&user_id, &meta_key)
.ok_or_else(|| AppError(anyhow!("team not found or access denied")))?;
let meta: serde_json::Value = serde_json::from_str(&record.facts_json).unwrap_or_default();
let members = meta["members"].as_array().map(|a| a.len()).unwrap_or(1);
let user = state
.db
.get_user(&user_id)
.ok_or_else(|| AppError(anyhow!("user not found")))?;
Ok(Json(TeamInfo {
team_id,
owner_id: meta["owner_id"].as_str().unwrap_or("").to_string(),
name: meta["name"].as_str().unwrap_or("").to_string(),
member_count: members,
tier: user.tier,
}))
}
pub async fn invite_member(
State(state): State<AppState>,
AuthUser(_user_id): AuthUser,
Path(team_id): Path<String>,
Json(req): Json<InviteMemberReq>,
) -> Result<Json<InviteMemberResp>, AppError> {
let conn = state.db.conn();
let invited_uid: Option<String> = conn
.query_row(
"SELECT id FROM users WHERE email=?1",
rusqlite::params![req.email],
|row| row.get(0),
)
.ok();
drop(conn);
if let Some(ref uid) = invited_uid {
let meta_key = format!("team_meta:{team_id}");
if let Some(record) = state.db.get_vault(&_user_id, &meta_key) {
let mut meta: serde_json::Value =
serde_json::from_str(&record.facts_json).unwrap_or_default();
if let Some(arr) = meta["members"].as_array_mut() {
if !arr.iter().any(|v| v.as_str() == Some(uid)) {
arr.push(serde_json::json!(uid));
}
}
state
.db
.upsert_vault(&_user_id, &meta_key, &meta.to_string())
.map_err(|e| AppError(anyhow!(e)))?;
}
}
Ok(Json(InviteMemberResp {
ok: invited_uid.is_some(),
invited_user_id: invited_uid,
}))
}
pub async fn push_team_vault(
State(state): State<AppState>,
AuthUser(user_id): AuthUser,
Path(team_id): Path<String>,
Json(req): Json<TeamVaultPushReq>,
) -> Result<Json<TeamVaultPushResp>, AppError> {
let key = team_vault_key(&team_id, &req.project_hash);
let accepted = req.facts.len();
let json = serde_json::to_string(&req.facts).map_err(|e| AppError(anyhow!(e)))?;
let version = state
.db
.upsert_vault(&user_id, &key, &json)
.map_err(|e| AppError(anyhow!(e)))?;
Ok(Json(TeamVaultPushResp {
accepted,
server_version: version as u64,
}))
}
pub async fn pull_team_vault(
State(state): State<AppState>,
AuthUser(user_id): AuthUser,
Path((team_id, project_hash)): Path<(String, String)>,
) -> Result<Json<TeamVaultPullResp>, AppError> {
let key = team_vault_key(&team_id, &project_hash);
match state.db.get_vault(&user_id, &key) {
Some(r) => {
let facts: Vec<MemoFact> = serde_json::from_str(&r.facts_json).unwrap_or_default();
Ok(Json(TeamVaultPullResp {
facts,
server_version: r.server_version as u64,
}))
}
None => Ok(Json(TeamVaultPullResp {
facts: vec![],
server_version: 0,
})),
}
}