Files
zhenyi 6205a6de0a refactor(models): replace hardcoded strings with typed enums
- Add ReviewState enum (pending, approved, changes_requested, etc.)
- Add DEFAULT_REVISION constant for git HEAD references
- service/pr/reviews.rs: use ReviewState for review creation and
  submission state validation
- service/pr/core.rs: use MergeStrategyKind for merge strategy
  selection
- service/im/stages.rs: use StagePrivacyLevel for stage creation
- service/im/invitations.rs: use Role enum for invitation role
  defaults
2026-06-10 18:49:06 +08:00

144 lines
4.9 KiB
Rust

use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use uuid::Uuid;
use crate::error::AppError;
use crate::models::channels::ChannelInvitation;
use crate::models::common::Role;
use crate::service::ImService;
use super::session::ImSession;
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct CreateInvitationParams {
pub invited_user_id: Option<Uuid>,
pub email: Option<String>,
pub role: Option<String>,
pub expires_in_hours: Option<i64>,
}
impl ImService {
pub async fn invitation_list(
&self,
_ctx: &ImSession,
channel_id: Uuid,
) -> Result<Vec<ChannelInvitation>, AppError> {
sqlx::query_as::<_, ChannelInvitation>(
"SELECT id, channel_id, workspace_id, invited_user_id, email, role, token_hash, \
invited_by, accepted_at, revoked_at, expires_at, created_at \
FROM channel_invitation WHERE channel_id = $1 \
AND accepted_at IS NULL AND revoked_at IS NULL \
ORDER BY created_at DESC",
)
.bind(channel_id)
.fetch_all(self.ctx.db.reader())
.await
.map_err(AppError::Database)
}
pub async fn invitation_create(
&self,
ctx: &ImSession,
channel_id: Uuid,
workspace_id: Uuid,
params: CreateInvitationParams,
) -> Result<ChannelInvitation, AppError> {
let now = chrono::Utc::now();
let expires_at = now + chrono::Duration::hours(params.expires_in_hours.unwrap_or(168));
let token = Uuid::now_v7().to_string();
let token_hash = format!("{:x}", Sha256::digest(token.as_bytes()));
let role = params
.role
.as_deref()
.and_then(|s| s.parse::<Role>().ok())
.filter(|r| *r != Role::Unknown)
.unwrap_or(Role::Member);
sqlx::query_as::<_, ChannelInvitation>(
"INSERT INTO channel_invitation \
(id, channel_id, workspace_id, invited_user_id, email, role, token_hash, \
invited_by, expires_at, created_at) \
VALUES ($1, $2, $3, $4, $5, $6::role, $7, $8, $9, $10) \
RETURNING id, channel_id, workspace_id, invited_user_id, email, role, token_hash, \
invited_by, accepted_at, revoked_at, expires_at, created_at",
)
.bind(Uuid::now_v7())
.bind(channel_id)
.bind(workspace_id)
.bind(params.invited_user_id)
.bind(params.email.as_deref())
.bind(role)
.bind(&token_hash)
.bind(ctx.user)
.bind(expires_at)
.bind(now)
.fetch_one(self.ctx.db.writer())
.await
.map_err(AppError::Database)
}
pub async fn invitation_accept(
&self,
ctx: &ImSession,
invitation_id: Uuid,
) -> Result<ChannelInvitation, AppError> {
let now = chrono::Utc::now();
let mut txn = self
.ctx
.db
.writer()
.begin()
.await
.map_err(|_| AppError::TxnError)?;
let invitation = sqlx::query_as::<_, ChannelInvitation>(
"UPDATE channel_invitation SET accepted_at = $1 \
WHERE id = $2 AND accepted_at IS NULL AND revoked_at IS NULL AND expires_at > $1 \
RETURNING id, channel_id, workspace_id, invited_user_id, email, role, token_hash, \
invited_by, accepted_at, revoked_at, expires_at, created_at",
)
.bind(now)
.bind(invitation_id)
.fetch_one(&mut *txn)
.await
.map_err(AppError::Database)?;
sqlx::query(
"INSERT INTO channel_member \
(id, channel_id, user_id, role, status, muted, pinned, joined_at, created_at, updated_at) \
VALUES ($1, $2, $3, $4::role, 'active', false, false, $5, $5, $5) \
ON CONFLICT (channel_id, user_id) DO NOTHING",
)
.bind(Uuid::now_v7())
.bind(invitation.channel_id)
.bind(ctx.user)
.bind(invitation.role.to_string())
.bind(now)
.execute(&mut *txn)
.await
.map_err(AppError::Database)?;
txn.commit().await.map_err(|_| AppError::TxnError)?;
Ok(invitation)
}
pub async fn invitation_revoke(
&self,
_ctx: &ImSession,
invitation_id: Uuid,
) -> Result<ChannelInvitation, AppError> {
let now = chrono::Utc::now();
sqlx::query_as::<_, ChannelInvitation>(
"UPDATE channel_invitation SET revoked_at = $1 \
WHERE id = $2 AND accepted_at IS NULL AND revoked_at IS NULL \
RETURNING id, channel_id, workspace_id, invited_user_id, email, role, token_hash, \
invited_by, accepted_at, revoked_at, expires_at, created_at",
)
.bind(now)
.bind(invitation_id)
.fetch_one(self.ctx.db.writer())
.await
.map_err(AppError::Database)
}
}