miryad_core/users/
group.rs1use chrono::Utc;
2use sea_orm::entity::prelude::*;
3use sea_orm::{ConnectionTrait, Set};
4
5use crate::users::membership;
6
7pub const ADMIN_GROUP_NAME: &str = "admin";
10
11#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
12#[sea_orm(table_name = "miryad_groups")]
13pub struct Model {
14 #[sea_orm(primary_key)]
15 pub id: i32,
16 #[sea_orm(unique)]
17 pub name: String,
18 pub created_at: DateTimeUtc,
19}
20
21#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
22pub enum Relation {}
23
24impl ActiveModelBehavior for ActiveModel {}
25
26pub type Group = Entity;
27
28pub async fn ensure_group<C: ConnectionTrait>(db: &C, name: &str) -> Result<i32, DbErr> {
31 if let Some(existing) = Entity::find().filter(Column::Name.eq(name)).one(db).await? {
32 return Ok(existing.id);
33 }
34
35 let active = ActiveModel {
36 name: Set(name.to_string()),
37 created_at: Set(Utc::now()),
38 ..Default::default()
39 };
40
41 match active.insert(db).await {
42 Ok(model) => Ok(model.id),
43 Err(_) => Entity::find()
44 .filter(Column::Name.eq(name))
45 .one(db)
46 .await?
47 .map(|g| g.id)
48 .ok_or_else(|| DbErr::RecordNotFound(format!("group {name} vanished"))),
49 }
50}
51
52pub async fn is_admin<C: ConnectionTrait>(db: &C, user_id: i32) -> Result<bool, DbErr> {
53 is_member(db, user_id, ADMIN_GROUP_NAME).await
54}
55
56pub async fn is_member<C: ConnectionTrait>(db: &C, user_id: i32, group_name: &str) -> Result<bool, DbErr> {
57 let Some(group) = Entity::find().filter(Column::Name.eq(group_name)).one(db).await? else {
58 return Ok(false);
59 };
60
61 let exists = membership::Entity::find()
62 .filter(membership::Column::UserId.eq(user_id))
63 .filter(membership::Column::GroupId.eq(group.id))
64 .one(db)
65 .await?
66 .is_some();
67 Ok(exists)
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use crate::migration::Migrator;
74 use crate::users::membership::sync_group_memberships;
75 use crate::users::user::resolve_user;
76 use sea_orm_migration::MigratorTrait;
77
78 async fn test_db() -> DatabaseConnection {
79 let db = sea_orm::Database::connect("sqlite::memory:")
80 .await
81 .expect("in-memory sqlite connects");
82 Migrator::up(&db, None).await.expect("migrations apply cleanly");
83 db
84 }
85
86 #[tokio::test]
87 async fn admin_group_is_seeded_by_migration() {
88 let db = test_db().await;
89 let admin_group = Entity::find()
90 .filter(Column::Name.eq(ADMIN_GROUP_NAME))
91 .one(&db)
92 .await
93 .expect("query succeeds");
94 assert!(admin_group.is_some());
95 }
96
97 #[tokio::test]
98 async fn is_member_true_for_member_false_otherwise() {
99 let db = test_db().await;
100 let user = resolve_user(&db, "sub-1", None).await.expect("resolve succeeds");
101 sync_group_memberships(&db, user.id, &["editors".to_string()])
102 .await
103 .expect("sync succeeds");
104
105 assert!(is_member(&db, user.id, "editors").await.expect("query succeeds"));
106 assert!(!is_member(&db, user.id, "admin").await.expect("query succeeds"));
107 }
108
109 #[tokio::test]
110 async fn is_member_false_for_unknown_group() {
111 let db = test_db().await;
112 let user = resolve_user(&db, "sub-1", None).await.expect("resolve succeeds");
113 assert!(
114 !is_member(&db, user.id, "does-not-exist")
115 .await
116 .expect("query succeeds")
117 );
118 }
119}