116 lines
3.9 KiB
Rust
116 lines
3.9 KiB
Rust
use crate::AppService;
|
|
use crate::error::AppError;
|
|
use chrono::{DateTime, Utc};
|
|
use models::projects::{MemberRole, project, project_audit_log, project_members};
|
|
use sea_orm::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use session::Session;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
|
pub struct ProjectInitParams {
|
|
pub name: String,
|
|
pub display_name: Option<String>,
|
|
pub description: Option<String>,
|
|
pub is_public: bool,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
|
pub struct ProjectInitResponse {
|
|
pub params: ProjectInitParams,
|
|
pub project: ProjectModel,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
|
pub struct ProjectModel {
|
|
pub uid: Uuid,
|
|
pub name: String,
|
|
pub display_name: String,
|
|
pub avatar_url: Option<String>,
|
|
pub description: Option<String>,
|
|
pub is_public: bool,
|
|
pub created_by: Uuid,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl AppService {
|
|
pub async fn project_init(
|
|
&self,
|
|
ctx: &Session,
|
|
params: ProjectInitParams,
|
|
) -> Result<ProjectInitResponse, AppError> {
|
|
let inner = params.clone();
|
|
if let Ok(_) = self.utils_find_project_by_name(params.name.clone()).await {
|
|
return Err(AppError::ProjectNameAlreadyExists);
|
|
}
|
|
let user = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let user = self.utils_find_user_by_uid(user).await?;
|
|
|
|
let project_uid = Uuid::now_v7();
|
|
let txn = self.db.begin().await?;
|
|
let project = project::ActiveModel {
|
|
id: Set(project_uid),
|
|
name: Set(params.name.clone()),
|
|
display_name: Set(params.display_name.unwrap_or(params.name.clone())),
|
|
avatar_url: Set(None),
|
|
description: Set(params.description),
|
|
is_public: Set(params.is_public),
|
|
created_by: Set(user.uid),
|
|
created_at: Set(Utc::now()),
|
|
updated_at: Set(Utc::now()),
|
|
};
|
|
let _project = project.insert(&txn).await?;
|
|
|
|
let project_member = project_members::ActiveModel {
|
|
id: Default::default(),
|
|
project: Set(_project.id),
|
|
user: Set(user.uid),
|
|
scope: Set(MemberRole::Owner.to_string()),
|
|
joined_at: Set(Utc::now()),
|
|
};
|
|
project_member.insert(&txn).await?;
|
|
|
|
let log = project_audit_log::ActiveModel {
|
|
project: Set(_project.id),
|
|
actor: Set(user.uid),
|
|
action: Set("project_create".to_string()),
|
|
details: Set(Some(serde_json::json!({
|
|
"project_name": _project.name.clone(),
|
|
"project_uid": _project.id,
|
|
"is_public": _project.is_public,
|
|
"description": _project.description.clone(),
|
|
}))),
|
|
created_at: Set(Utc::now()),
|
|
..Default::default()
|
|
};
|
|
log.insert(&txn).await?;
|
|
|
|
txn.commit().await?;
|
|
|
|
observability::incr!(observability::PROJECTS_CREATED_TOTAL);
|
|
|
|
// Initialize project billing ($20 for first project, $0 otherwise)
|
|
if let Err(e) =
|
|
agent::billing::initialize_project_billing(&self.db, _project.id, user.uid).await
|
|
{
|
|
tracing::warn!(project_id = %_project.id, error = %e, "Failed to initialize project billing — non-critical, continuing");
|
|
}
|
|
|
|
Ok(ProjectInitResponse {
|
|
params: inner,
|
|
project: ProjectModel {
|
|
uid: _project.id,
|
|
name: _project.name.clone(),
|
|
display_name: _project.display_name.clone(),
|
|
avatar_url: _project.avatar_url.clone(),
|
|
description: _project.description.clone(),
|
|
is_public: _project.is_public,
|
|
created_by: _project.created_by,
|
|
created_at: _project.created_at,
|
|
updated_at: _project.updated_at,
|
|
},
|
|
})
|
|
}
|
|
}
|