//! Who may do what in an area. Areas are members-only: to anyone who isn't a
//! member, an area and everything in it (restaurants, lunches, outings)
//! answers 404, as though it didn't exist. Members are held to their role
//! with 403. See `entities::area_members::Role` for what each role allows.
use std::collections::HashMap;
use axum::http::StatusCode;
use chrono::Utc;
use sea_orm::sea_query::OnConflict;
use sea_orm::*;
use tracing::error;
use uuid::Uuid;
use crate::entities::area_members::{self, Role};
use crate::entities::{areas, users};
pub async fn role_in(db: &impl ConnectionTrait, area_id: Uuid, user_id: Uuid) -> Result<Option<Role>, DbErr> {
Ok(area_members::Entity::find_by_id((area_id, user_id)).one(db).await?.map(|m| m.role))
}
/// The user's role in the area, if it's at least `min`. Not a member: 404;
/// a member with too little: 403.
pub async fn require(db: &impl ConnectionTrait, area_id: Uuid, user_id: Uuid, min: Role) -> Result<Role, StatusCode> {
match role_in(db, area_id, user_id).await {
Ok(Some(role)) if role >= min => Ok(role),
Ok(Some(_)) => Err(StatusCode::FORBIDDEN),
Ok(None) => Err(StatusCode::NOT_FOUND),
Err(e) => {
error!(error = %e, "load membership failed");
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
/// Areas the user belongs to, by name, with their role in each.
pub async fn member_areas(db: &impl ConnectionTrait, user_id: Uuid) -> Result<Vec<(areas::Model, Role)>, DbErr> {
let roles: HashMap<Uuid, Role> = area_members::Entity::find()
.filter(area_members::Column::UserId.eq(user_id))
.all(db)
.await?
.into_iter()
.map(|m| (m.area_id, m.role))
.collect();
Ok(areas::Entity::find()
.filter(areas::Column::Id.is_in(roles.keys().copied()))
.order_by_asc(areas::Column::Name)
.all(db)
.await?
.into_iter()
.map(|a| {
let role = roles[&a.id];
(a, role)
})
.collect())
}
/// An area's active members, by username, with roles.
pub async fn members(db: &impl ConnectionTrait, area_id: Uuid) -> Result<Vec<(users::Model, Role)>, DbErr> {
let roles: HashMap<Uuid, Role> = area_members::Entity::find()
.filter(area_members::Column::AreaId.eq(area_id))
.all(db)
.await?
.into_iter()
.map(|m| (m.user_id, m.role))
.collect();
Ok(users::Entity::find()
.filter(users::Column::Id.is_in(roles.keys().copied()))
.filter(users::Column::IsActive.eq(true))
.order_by_asc(users::Column::Username)
.all(db)
.await?
.into_iter()
.map(|u| {
let role = roles[&u.id];
(u, role)
})
.collect())
}
/// Make someone a member with at least `role`. An existing higher role is
/// kept: an invite never demotes.
pub async fn grant(db: &impl ConnectionTrait, area_id: Uuid, user_id: Uuid, role: Role) -> Result<(), DbErr> {
let role = match role_in(db, area_id, user_id).await? {
Some(current) => current.max(role),
None => role,
};
area_members::Entity::insert(area_members::ActiveModel {
area_id: Set(area_id),
user_id: Set(user_id),
role: Set(role),
created_at: Set(Utc::now().into()),
})
.on_conflict(
OnConflict::columns([area_members::Column::AreaId, area_members::Column::UserId])
.update_column(area_members::Column::Role)
.to_owned(),
)
.exec_without_returning(db)
.await?;
Ok(())
}
/// Admins in the area — an area must always keep at least one.
pub async fn admin_count(db: &impl ConnectionTrait, area_id: Uuid) -> Result<u64, DbErr> {
area_members::Entity::find()
.filter(area_members::Column::AreaId.eq(area_id))
.filter(area_members::Column::Role.eq(Role::Admin))
.count(db)
.await
}
neiam
/fieri-forever
the flavortown lunch engine
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
3.9 KiB · text
History
7a00c02