use axum::{extract::State, http::HeaderMap, Json};
use std::sync::Arc;
use crate::callback::AuthCallback;
use crate::errors::AppError;
use crate::models::{AcceptInviteRequest, AcceptInviteResponse};
use crate::repositories::{hash_invite_token, AuditEventType, MembershipEntity};
use crate::services::EmailService;
use crate::utils::authenticate;
use crate::AppState;
pub async fn accept_invite<C: AuthCallback, E: EmailService>(
State(state): State<Arc<AppState<C, E>>>,
headers: HeaderMap,
Json(req): Json<AcceptInviteRequest>,
) -> Result<Json<AcceptInviteResponse>, AppError> {
let auth = authenticate(&state, &headers).await?;
let user = state
.user_repo
.find_by_id(auth.user_id)
.await?
.ok_or(AppError::InvalidToken)?;
let token_hash = hash_invite_token(&req.token);
let invite = state
.invite_repo
.find_by_token_hash(&token_hash)
.await?
.ok_or(AppError::NotFound("Invalid or expired invite".into()))?;
let user_email = user.email.as_ref().ok_or(AppError::Validation(
"You must have an email to accept invites".into(),
))?;
if user_email.to_lowercase() != invite.email.to_lowercase() {
return Err(AppError::Forbidden(
"This invite was sent to a different email address".into(),
));
}
if state
.membership_repo
.find_by_user_and_org(auth.user_id, invite.org_id)
.await?
.is_some()
{
return Err(AppError::Validation(
"You are already a member of this organization".into(),
));
}
let accepted_invite = state
.invite_repo
.mark_accepted_if_valid(invite.id)
.await?
.ok_or(AppError::Validation(
"Invite has already been accepted or has expired".into(),
))?;
let org = state
.org_repo
.find_by_id(accepted_invite.org_id)
.await?
.ok_or(AppError::NotFound("Organization not found".into()))?;
let membership =
MembershipEntity::new(auth.user_id, accepted_invite.org_id, accepted_invite.role);
if let Err(err) = state.membership_repo.create(membership).await {
if let Err(rollback_err) = state.invite_repo.unmark_accepted(accepted_invite.id).await {
tracing::error!(
invite_id = %accepted_invite.id,
user_id = %auth.user_id,
org_id = %accepted_invite.org_id,
original_error = %err,
rollback_error = %rollback_err,
"CRITICAL: Invite rollback failed - invite marked accepted but no membership created. Manual intervention required."
);
let _ = state
.audit_service
.log_org_event(
AuditEventType::InviteRollbackFailed,
auth.user_id,
accepted_invite.org_id,
Some(&headers),
)
.await;
}
return Err(err);
}
Ok(Json(AcceptInviteResponse {
org_id: org.id,
org_name: org.name,
role: accepted_invite.role.as_str().to_string(),
}))
}