//! Per-repo collaborator access queries. use chrono::{DateTime, Utc}; use sqlx::{FromRow, PgPool}; use uuid::Uuid; use super::{GitRepoId, UserId}; use crate::error::Result; #[derive(Debug, FromRow)] pub struct DbRepoCollaborator { pub id: Uuid, pub repo_id: GitRepoId, pub user_id: UserId, pub can_push: bool, pub created_at: DateTime, } /// Collaborator with joined username for display. #[derive(Debug, FromRow)] pub struct CollaboratorWithUsername { pub id: Uuid, pub user_id: UserId, pub username: String, pub can_push: bool, pub created_at: DateTime, } /// Add a collaborator to a repo. Returns the new record. #[tracing::instrument(skip_all)] pub async fn add_collaborator( pool: &PgPool, repo_id: GitRepoId, user_id: UserId, can_push: bool, ) -> Result { let row = sqlx::query_as::<_, DbRepoCollaborator>( r#" INSERT INTO repo_collaborators (repo_id, user_id, can_push) VALUES ($1, $2, $3) RETURNING * "#, ) .bind(repo_id) .bind(user_id) .bind(can_push) .fetch_one(pool) .await?; Ok(row) } /// Remove a collaborator from a repo. Returns true if a row was deleted. #[tracing::instrument(skip_all)] pub async fn remove_collaborator( pool: &PgPool, repo_id: GitRepoId, user_id: UserId, ) -> Result { let result = sqlx::query( "DELETE FROM repo_collaborators WHERE repo_id = $1 AND user_id = $2", ) .bind(repo_id) .bind(user_id) .execute(pool) .await?; Ok(result.rows_affected() > 0) } /// List collaborators for a repo, with usernames. #[tracing::instrument(skip_all)] pub async fn list_collaborators( pool: &PgPool, repo_id: GitRepoId, ) -> Result> { let rows = sqlx::query_as::<_, CollaboratorWithUsername>( r#" SELECT rc.id, rc.user_id, u.username, rc.can_push, rc.created_at FROM repo_collaborators rc JOIN users u ON u.id = rc.user_id WHERE rc.repo_id = $1 ORDER BY rc.created_at ASC "#, ) .bind(repo_id) .fetch_all(pool) .await?; Ok(rows) } /// Check if a user has push access to a repo (either owner or collaborator with can_push). #[tracing::instrument(skip_all)] pub async fn can_user_push( pool: &PgPool, repo_id: GitRepoId, user_id: UserId, ) -> Result { let row: (bool,) = sqlx::query_as( r#" SELECT EXISTS( SELECT 1 FROM repo_collaborators WHERE repo_id = $1 AND user_id = $2 AND can_push = true ) "#, ) .bind(repo_id) .bind(user_id) .fetch_one(pool) .await?; Ok(row.0) } /// Check if a user has read access to a repo (any collaborator record, regardless of can_push). #[tracing::instrument(skip_all)] pub async fn is_collaborator( pool: &PgPool, repo_id: GitRepoId, user_id: UserId, ) -> Result { let row: (bool,) = sqlx::query_as( r#" SELECT EXISTS( SELECT 1 FROM repo_collaborators WHERE repo_id = $1 AND user_id = $2 ) "#, ) .bind(repo_id) .bind(user_id) .fetch_one(pool) .await?; Ok(row.0) }