194c44c
use models::dir::dirs::{self, DirKind};
use models::issue::issue_item;
use models::issue::issues;
use models::space::spaces;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use space::share::authz::{READ, VIEW_SPACE, effective_permissions};
use uuid::Uuid;
use crate::errors::IssueError;
use crate::services::service::IssueService;
pub async fn lookup_space_by_name(
service: &IssueService,
space_name: &str,
) -> Result<spaces::Model, IssueError> {
let lower = space_name.trim().to_lowercase();
if lower.is_empty() {
return Err(IssueError::NotFound);
}
spaces::Entity::find()
.filter(spaces::Column::LowerName.eq(&lower))
.filter(spaces::Column::IsActive.eq(true))
.one(service.db().read())
.await?
.ok_or(IssueError::NotFound)
}
pub async fn lookup_board(
service: &IssueService,
space: &spaces::Model,
board_id: Uuid,
caller: i64,
) -> Result<issues::Model, IssueError> {
let board = issues::Entity::find_by_id(board_id)
.one(service.db().read())
.await?
.ok_or(IssueError::NotFound)?;
let dir = dirs::Entity::find_by_id(board.dir_id)
.filter(dirs::Column::SpaceId.eq(Some(space.id)))
.one(service.db().read())
.await?
.ok_or(IssueError::NotFound)?;
if dir.kind != DirKind::Object {
return Err(IssueError::NotFound);
}
if !space::share::dir_visibility::is_dir_visible(
service.db().read(),
space,
board.dir_id,
caller,
)
.await?
{
return Err(IssueError::NotFound);
}
Ok(board)
}
pub async fn lookup_issue(
service: &IssueService,
board: &issues::Model,
issue_id: Uuid,
) -> Result<issue_item::Model, IssueError> {
issue_item::Entity::find_by_id(issue_id)
.filter(issue_item::Column::CollectionId.eq(board.id))
.one(service.db().read())
.await?
.ok_or(IssueError::NotFound)
}
pub async fn lookup_issue_by_ref(
service: &IssueService,
board: &issues::Model,
issue_ref: &str,
) -> Result<issue_item::Model, IssueError> {
let base = issue_item::Entity::find().filter(issue_item::Column::CollectionId.eq(board.id));
let item = if let Ok(id) = Uuid::parse_str(issue_ref) {
base.filter(issue_item::Column::Id.eq(id))
.one(service.db().read())
.await?
} else if let Ok(number) = issue_ref.parse::<i32>() {
base.filter(issue_item::Column::AutoincrementId.eq(number))
.one(service.db().read())
.await?
} else if let Some(number) = board_prefix_number(board.prefix.as_deref(), issue_ref) {
// `CAD-12` keeps pasted links readable; only this board's own prefix
// resolves, so a foreign key never silently points at another board.
base.filter(issue_item::Column::AutoincrementId.eq(number))
.one(service.db().read())
.await?
} else {
None
};
item.ok_or(IssueError::NotFound)
}
/// `PREFIX-12` (case-insensitive) → `Some(12)` when the prefix matches the
/// board's own; anything else is left to the plain-number path.
fn board_prefix_number(prefix: Option<&str>, issue_ref: &str) -> Option<i32> {
let prefix = prefix?;
let (head, tail) = issue_ref.rsplit_once('-')?;
if !head.eq_ignore_ascii_case(prefix) {
return None;
}
tail.parse::<i32>().ok()
}
pub async fn require_member(
service: &IssueService,
space: &spaces::Model,
caller: i64,
) -> Result<(), IssueError> {
let perms = effective_permissions(&service.state, space, caller)
.await
.map_err(IssueError::from)?;
if perms.has(VIEW_SPACE) || perms.has(READ) {
Ok(())
} else {
Err(IssueError::Forbidden)
}
}
pub async fn space_context(
service: &IssueService,
caller: i64,
space_name: &str,
) -> Result<spaces::Model, IssueError> {
let space = lookup_space_by_name(service, space_name).await?;
require_member(service, &space, caller).await?;
Ok(space)
}
pub async fn board_context(
service: &IssueService,
caller: i64,
space_name: &str,
board_id: Uuid,
) -> Result<(spaces::Model, issues::Model), IssueError> {
let space = space_context(service, caller, space_name).await?;
let board = lookup_board(service, &space, board_id, caller).await?;
Ok((space, board))
}
pub async fn issue_context(
service: &IssueService,
caller: i64,
space_name: &str,
board_id: Uuid,
issue_id: Uuid,
) -> Result<(spaces::Model, issues::Model, issue_item::Model), IssueError> {
let (space, board) = board_context(service, caller, space_name, board_id).await?;
let item = lookup_issue(service, &board, issue_id).await?;
Ok((space, board, item))
}
pub async fn require_issue_access(
service: &IssueService,
caller: i64,
space_name: &str,
board_id: Uuid,
issue_id: Uuid,
) -> Result<(), IssueError> {
issue_context(service, caller, space_name, board_id, issue_id).await?;
Ok(())
}
/// Same access rules as [`issue_context`], resolved from the issue id alone —
/// used by routes that carry no space/board in their path (attachment reads).
pub async fn issue_context_by_id(
service: &IssueService,
caller: i64,
issue_id: Uuid,
) -> Result<(spaces::Model, issues::Model, issue_item::Model), IssueError> {
let item = issue_item::Entity::find_by_id(issue_id)
.one(service.db().read())
.await?
.ok_or(IssueError::NotFound)?;
let board = issues::Entity::find_by_id(item.collection_id)
.one(service.db().read())
.await?
.ok_or(IssueError::NotFound)?;
let dir = dirs::Entity::find_by_id(board.dir_id)
.one(service.db().read())
.await?
.ok_or(IssueError::NotFound)?;
let space = spaces::Entity::find_by_id(dir.space_id.ok_or(IssueError::NotFound)?)
.filter(spaces::Column::IsActive.eq(true))
.one(service.db().read())
.await?
.ok_or(IssueError::NotFound)?;
require_member(service, &space, caller).await?;
let board = lookup_board(service, &space, board.id, caller).await?;
Ok((space, board, item))
}