1use crate::error::{human_bytes, Error, Result};
30use crate::model::{now_ms, Level, PrincipalKind, UnixMillis};
31
32use super::{AuthDoc, AuthTable};
33
34#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct Clan {
39 pub name: String,
41 pub created_by: String,
43 pub created_at: UnixMillis,
45 #[serde(default)]
47 pub members: Vec<ClanMember>,
48 #[serde(default, skip_serializing_if = "Vec::is_empty")]
50 pub invites: Vec<ClanInvite>,
51 #[serde(default, skip_serializing_if = "Vec::is_empty")]
53 pub offers: Vec<ClanRepoOffer>,
54}
55
56#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
60#[serde(deny_unknown_fields)]
61pub struct ClanRepoOffer {
62 pub repo: String,
64 pub to: String,
66 pub by: String,
68 pub at: UnixMillis,
70}
71
72#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
74#[serde(deny_unknown_fields)]
75pub struct ClanMember {
76 pub principal: String,
78 #[serde(default)]
80 pub admin: bool,
81 #[serde(default)]
83 pub contribution_bytes: u64,
84 pub joined_at: UnixMillis,
86}
87
88#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
90#[serde(deny_unknown_fields)]
91pub struct ClanInvite {
92 pub principal: String,
94 pub by: String,
96 pub at: UnixMillis,
98}
99
100#[derive(Clone, Copy, PartialEq, Eq, Debug)]
102pub enum ClanRole {
103 Admin,
105 Member,
107 Invited,
109}
110
111#[derive(Clone, Copy, PartialEq, Eq, Debug)]
113pub enum ClanRefusal {
114 NotFound,
116 Denied,
118 Precondition,
120 Invalid,
122 Exists,
124}
125
126#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
129pub struct AccountRoom {
130 pub cap: Option<u64>,
132 pub stored: u64,
134}
135
136impl Clan {
137 pub fn room_bytes(&self) -> u64 {
139 self.members
140 .iter()
141 .map(|m| m.contribution_bytes)
142 .fold(0, u64::saturating_add)
143 }
144
145 pub fn member(&self, principal: &str) -> Option<&ClanMember> {
147 self.members.iter().find(|m| m.principal == principal)
148 }
149
150 pub fn role_of(&self, principal: &str) -> Option<ClanRole> {
152 match self.member(principal) {
153 Some(m) if m.admin => Some(ClanRole::Admin),
154 Some(_) => Some(ClanRole::Member),
155 None if self.invites.iter().any(|i| i.principal == principal) => {
156 Some(ClanRole::Invited)
157 }
158 None => None,
159 }
160 }
161
162 pub fn admins(&self) -> usize {
164 self.members.iter().filter(|m| m.admin).count()
165 }
166
167 pub fn floor_for(&self, member: &str, used: u64) -> u64 {
171 self.floor_in_force(member, used, &[])
172 }
173
174 pub fn floor_in_force(&self, member: &str, used: u64, lapsed: &[String]) -> u64 {
179 if lapsed.iter().any(|l| l == member) {
180 return 0;
181 }
182 let others: u64 = self
183 .members
184 .iter()
185 .filter(|m| m.principal != member && !lapsed.contains(&m.principal))
186 .map(|m| m.contribution_bytes)
187 .fold(0, u64::saturating_add);
188 used.saturating_sub(others)
189 }
190
191 pub fn room_in_force(&self, lapsed: &[String]) -> u64 {
194 self.members
195 .iter()
196 .filter(|m| !lapsed.contains(&m.principal))
197 .map(|m| m.contribution_bytes)
198 .fold(0, u64::saturating_add)
199 }
200
201 pub fn offer_of(&self, repo: &str) -> Option<&ClanRepoOffer> {
203 self.offers.iter().find(|o| o.repo == repo)
204 }
205}
206
207fn refused(clan: &str, refusal: ClanRefusal, reason: String) -> Error {
208 Error::Clan {
209 clan: clan.to_owned(),
210 refusal,
211 reason,
212 }
213}
214
215impl AuthTable {
216 pub fn clan(&self, name: &str) -> Result<Option<Clan>> {
220 let doc = self.doc.load()?;
221 if doc.clans.is_empty() {
222 return Ok(None);
223 }
224 doc.clans.get(name)
225 }
226
227 pub fn is_clan(&self, name: &str) -> Result<bool> {
229 Ok(self.clan(name)?.is_some())
230 }
231
232 pub fn clans_of(&self, principal: &str) -> Result<Vec<Clan>> {
234 let doc = self.doc.load()?;
235 let mut out = Vec::new();
236 for name in doc.clan_index.get(principal)?.unwrap_or_default() {
237 if let Some(clan) = doc.clans.get(&name)? {
238 out.push(clan);
239 }
240 }
241 Ok(out)
242 }
243
244 pub fn clan_role(&self, principal: &str, namespace: &str) -> Result<Option<ClanRole>> {
247 Ok(self
248 .clan(namespace)?
249 .and_then(|clan| clan.role_of(principal)))
250 }
251
252 pub fn contributed_by(&self, account: &str) -> Result<u64> {
255 let doc = self.doc.load()?;
256 Self::contributed_in(&doc, account, None)
257 }
258
259 pub fn room_left(&self, account: &str, room: AccountRoom) -> Result<Option<u64>> {
263 let Some(cap) = room.cap else { return Ok(None) };
264 let doc = self.doc.load()?;
265 let committed = room
266 .stored
267 .saturating_add(Self::promised_by(&doc, account)?)
268 .saturating_add(Self::contributed_in(&doc, account, None)?);
269 Ok(Some(cap.saturating_sub(committed)))
270 }
271
272 pub(super) fn clan_level(
277 doc: &AuthDoc,
278 principal: &str,
279 namespace: &str,
280 ) -> Result<Option<Level>> {
281 if doc.clans.is_empty() {
282 return Ok(None);
283 }
284 let Some(clan) = doc.clans.get(namespace)? else {
285 return Ok(None);
286 };
287 Ok(clan
288 .member(principal)
289 .map(|m| if m.admin { Level::Admin } else { Level::Write }))
290 }
291
292 pub(super) fn contributed_in(
294 doc: &AuthDoc,
295 account: &str,
296 except: Option<&str>,
297 ) -> Result<u64> {
298 if doc.clan_index.is_empty() {
299 return Ok(0);
300 }
301 let mut sum: u64 = 0;
302 for name in doc.clan_index.get(account)?.unwrap_or_default() {
303 if Some(name.as_str()) == except {
304 continue;
305 }
306 if let Some(clan) = doc.clans.get(&name)? {
307 if let Some(m) = clan.member(account) {
308 sum = sum.saturating_add(m.contribution_bytes);
309 }
310 }
311 }
312 Ok(sum)
313 }
314
315 pub fn clan_for(&self, clan: &str, by: &str) -> Result<Clan> {
319 let doc = self.doc.load()?;
320 Self::clan_for_admin(&doc, clan, by, "dissolve it")
321 }
322
323 pub fn create_clan(
330 &self,
331 name: &str,
332 creator: &str,
333 contribution: u64,
334 room: AccountRoom,
335 ) -> Result<Clan> {
336 crate::sandbox::check_component(name, name)?;
337 if name == crate::model::ANONYMOUS_PRINCIPAL_ID {
338 return Err(Error::ReservedPrincipalId(name.to_owned()));
339 }
340 let at = now_ms();
341 self.doc.update(|doc| {
342 Self::ordinary_account(doc, name, creator, "make a clan")?;
343 if doc.clans.contains_key(name)? {
344 return Err(refused(
345 name,
346 ClanRefusal::Exists,
347 format!("a clan called {name}"),
348 ));
349 }
350 if doc.principals.contains_key(name)? {
351 return Err(refused(
352 name,
353 ClanRefusal::Exists,
354 format!("an account called {name}"),
355 ));
356 }
357 Self::fits_in_room(doc, name, creator, contribution, room)?;
358 let clan = Clan {
359 name: name.to_owned(),
360 created_by: creator.to_owned(),
361 created_at: at,
362 members: vec![ClanMember {
363 principal: creator.to_owned(),
364 admin: true,
365 contribution_bytes: contribution,
366 joined_at: at,
367 }],
368 invites: Vec::new(),
369 offers: Vec::new(),
370 };
371 doc.clans.insert(name.to_owned(), clan.clone())?;
372 Self::index_clan(doc, creator, name)?;
373 Ok(clan)
374 })
375 }
376
377 pub fn invite_to_clan(&self, clan: &str, by: &str, principal: &str) -> Result<Clan> {
380 let at = now_ms();
381 self.doc.update(|doc| {
382 let mut row = Self::clan_for_admin(doc, clan, by, "invite an account")?;
383 Self::ordinary_account(doc, clan, principal, "join a clan")?;
384 match row.role_of(principal) {
385 Some(ClanRole::Admin | ClanRole::Member) => {
386 return Err(refused(
387 clan,
388 ClanRefusal::Precondition,
389 format!("{principal} is already in clan {clan}"),
390 ))
391 }
392 Some(ClanRole::Invited) => {
393 return Err(refused(
394 clan,
395 ClanRefusal::Precondition,
396 format!(
397 "{principal} is already invited to clan {clan}, and has not answered"
398 ),
399 ))
400 }
401 None => {}
402 }
403 row.invites.push(ClanInvite {
404 principal: principal.to_owned(),
405 by: by.to_owned(),
406 at,
407 });
408 doc.clans.insert(clan.to_owned(), row.clone())?;
409 Self::index_clan(doc, principal, clan)?;
410 Ok(row)
411 })
412 }
413
414 pub fn answer_clan_invite(
419 &self,
420 clan: &str,
421 principal: &str,
422 accept: bool,
423 contribution: u64,
424 room: AccountRoom,
425 ) -> Result<Clan> {
426 let at = now_ms();
427 self.doc.update(|doc| {
428 let mut row = Self::visible_clan(doc, clan, principal)?;
429 if row.role_of(principal) != Some(ClanRole::Invited) {
430 return Err(refused(
431 clan,
432 ClanRefusal::Precondition,
433 format!("{principal} has no invitation to clan {clan} to answer"),
434 ));
435 }
436 row.invites.retain(|i| i.principal != principal);
437 if accept {
438 Self::ordinary_account(doc, clan, principal, "join a clan")?;
443 Self::fits_in_room(doc, clan, principal, contribution, room)?;
444 row.members.push(ClanMember {
445 principal: principal.to_owned(),
446 admin: false,
447 contribution_bytes: contribution,
448 joined_at: at,
449 });
450 } else {
451 Self::unindex_clan(doc, principal, clan)?;
452 }
453 doc.clans.insert(clan.to_owned(), row.clone())?;
454 Ok(row)
455 })
456 }
457
458 pub fn withdraw_clan_invite(&self, clan: &str, by: &str, principal: &str) -> Result<Clan> {
460 self.doc.update(|doc| {
461 let mut row = Self::clan_for_admin(doc, clan, by, "withdraw an invitation")?;
462 if row.role_of(principal) != Some(ClanRole::Invited) {
463 return Err(refused(
464 clan,
465 ClanRefusal::Precondition,
466 format!("{principal} has no invitation to clan {clan}"),
467 ));
468 }
469 row.invites.retain(|i| i.principal != principal);
470 Self::unindex_clan(doc, principal, clan)?;
471 doc.clans.insert(clan.to_owned(), row.clone())?;
472 Ok(row)
473 })
474 }
475
476 pub fn set_clan_contribution(
481 &self,
482 clan: &str,
483 member: &str,
484 bytes: u64,
485 room: AccountRoom,
486 used: Option<u64>,
487 lapsed: &[String],
488 ) -> Result<Clan> {
489 self.doc.update(|doc| {
490 let mut row = Self::visible_clan(doc, clan, member)?;
491 Self::refuse_gated(doc, clan, member)?;
492 let Some(now) = row.member(member).map(|m| m.contribution_bytes) else {
493 return Err(refused(
494 clan,
495 ClanRefusal::Denied,
496 format!("{member} is not a member of clan {clan}: only a member gives it room"),
497 ));
498 };
499 if bytes > now {
500 Self::fits_in_room(doc, clan, member, bytes, room)?;
501 } else if bytes < now {
502 Self::above_floor(
503 &row,
504 clan,
505 member,
506 bytes,
507 used,
508 lapsed,
509 "lower its contribution",
510 )?;
511 }
512 for m in &mut row.members {
513 if m.principal == member {
514 m.contribution_bytes = bytes;
515 }
516 }
517 doc.clans.insert(clan.to_owned(), row.clone())?;
518 Ok(row)
519 })
520 }
521
522 pub fn leave_clan(
526 &self,
527 clan: &str,
528 member: &str,
529 used: Option<u64>,
530 lapsed: &[String],
531 ) -> Result<Clan> {
532 self.doc.update(|doc| {
533 let mut row = Self::visible_clan(doc, clan, member)?;
534 Self::refuse_gated(doc, clan, member)?;
535 let Some(me) = row.member(member).cloned() else {
536 return Err(refused(clan, ClanRefusal::Precondition, format!("{member} is not a member of clan {clan}")));
537 };
538 if me.admin && row.admins() == 1 {
539 return Err(refused(
540 clan,
541 ClanRefusal::Precondition,
542 format!(
543 "{member} is clan {clan}'s last admin, and the last admin cannot leave: make another \
544 member admin first, or dissolve the clan"
545 ),
546 ));
547 }
548 Self::above_floor(&row, clan, member, 0, used, lapsed, "leave")?;
549 row.members.retain(|m| m.principal != member);
550 Self::unindex_clan(doc, member, clan)?;
551 doc.clans.insert(clan.to_owned(), row.clone())?;
552 Ok(row)
553 })
554 }
555
556 pub fn remove_from_clan(
560 &self,
561 clan: &str,
562 by: &str,
563 member: &str,
564 used: Option<u64>,
565 lapsed: &[String],
566 ) -> Result<Clan> {
567 self.doc.update(|doc| {
568 let mut row = Self::clan_for_admin(doc, clan, by, "remove a member")?;
569 if member == by {
570 return Err(refused(
571 clan,
572 ClanRefusal::Invalid,
573 format!("{by} cannot remove itself from clan {clan}: that is leaving"),
574 ));
575 }
576 if row.member(member).is_none() {
577 return Err(refused(
578 clan,
579 ClanRefusal::Precondition,
580 format!("{member} is not a member of clan {clan}"),
581 ));
582 }
583 Self::above_floor(&row, clan, member, 0, used, lapsed, "be removed")?;
584 row.members.retain(|m| m.principal != member);
585 Self::unindex_clan(doc, member, clan)?;
586 doc.clans.insert(clan.to_owned(), row.clone())?;
587 Ok(row)
588 })
589 }
590
591 pub fn set_clan_admin(&self, clan: &str, by: &str, member: &str, admin: bool) -> Result<Clan> {
594 self.doc.update(|doc| {
595 let mut row = Self::clan_for_admin(doc, clan, by, "change who administers it")?;
596 let Some(was) = row.member(member).map(|m| m.admin) else {
597 return Err(refused(
598 clan,
599 ClanRefusal::Precondition,
600 format!("{member} is not a member of clan {clan}"),
601 ));
602 };
603 if was && !admin && row.admins() == 1 {
604 return Err(refused(
605 clan,
606 ClanRefusal::Precondition,
607 format!(
608 "{member} is clan {clan}'s last admin: make another member admin first"
609 ),
610 ));
611 }
612 for m in &mut row.members {
613 if m.principal == member {
614 m.admin = admin;
615 }
616 }
617 doc.clans.insert(clan.to_owned(), row.clone())?;
618 Ok(row)
619 })
620 }
621
622 pub fn dissolve_clan(&self, clan: &str, by: &str) -> Result<Clan> {
626 self.doc.update(|doc| {
627 let row = Self::clan_for_admin(doc, clan, by, "dissolve it")?;
628 for m in &row.members {
629 Self::unindex_clan(doc, &m.principal, clan)?;
630 }
631 for i in &row.invites {
632 Self::unindex_clan(doc, &i.principal, clan)?;
633 }
634 doc.clans.remove(clan)?;
635 Ok(row)
636 })
637 }
638
639 pub fn offer_clan_repo(&self, clan: &str, by: &str, repo: &str, to: &str) -> Result<Clan> {
647 let at = now_ms();
648 self.doc.update(|doc| {
649 let mut row = Self::clan_for_admin(doc, clan, by, "move a repository out of it")?;
650 if to == clan || doc.clans.contains_key(to)? {
651 return Err(refused(
652 clan,
653 ClanRefusal::Invalid,
654 format!("{to} is a clan: a repository moves out of a clan into an account"),
655 ));
656 }
657 let Some(account) = doc.principals.get(to)? else {
658 return Err(refused(
659 clan,
660 ClanRefusal::NotFound,
661 format!("there is no account {to}"),
662 ));
663 };
664 if account.kind != PrincipalKind::User || doc.employers.contains_key(to)? {
665 return Err(refused(
666 clan,
667 ClanRefusal::Precondition,
668 format!("{to} holds no account of its own to move {clan}/{repo} into"),
669 ));
670 }
671 if let Some(open) = row.offer_of(repo) {
672 return Err(refused(
673 clan,
674 ClanRefusal::Precondition,
675 format!(
676 "{clan}/{repo} is already offered to {}; withdraw that offer first",
677 open.to
678 ),
679 ));
680 }
681 row.offers.push(ClanRepoOffer {
682 repo: repo.to_owned(),
683 to: to.to_owned(),
684 by: by.to_owned(),
685 at,
686 });
687 doc.clans.insert(clan.to_owned(), row.clone())?;
688 Ok(row)
689 })
690 }
691
692 pub fn clan_repo_offer(&self, clan: &str, repo: &str) -> Result<Option<ClanRepoOffer>> {
694 Ok(self.clan(clan)?.and_then(|row| row.offer_of(repo).cloned()))
695 }
696
697 pub fn clan_offers_to(&self, to: &str) -> Result<Vec<(String, ClanRepoOffer)>> {
700 let doc = self.doc.load()?;
701 if doc.clans.is_empty() {
702 return Ok(Vec::new());
703 }
704 let mut out = Vec::new();
705 for (name, row) in doc.clans.entries()? {
706 for offer in row.offers {
707 if offer.to == to {
708 out.push((name.clone(), offer));
709 }
710 }
711 }
712 Ok(out)
713 }
714
715 pub fn withdraw_clan_repo_offer(&self, clan: &str, by: &str, repo: &str) -> Result<Clan> {
717 self.doc.update(|doc| {
718 let mut row = Self::clan_for_admin(doc, clan, by, "withdraw an offer")?;
719 if row.offer_of(repo).is_none() {
720 return Err(refused(
721 clan,
722 ClanRefusal::Precondition,
723 format!("{clan}/{repo} is offered to nobody"),
724 ));
725 }
726 row.offers.retain(|o| o.repo != repo);
727 doc.clans.insert(clan.to_owned(), row.clone())?;
728 Ok(row)
729 })
730 }
731
732 pub fn close_clan_repo_offer(&self, clan: &str, repo: &str, to: &str) -> Result<ClanRepoOffer> {
737 self.doc.update(|doc| {
738 let mut row = doc.clans.get(clan)?.ok_or_else(|| {
739 refused(
740 clan,
741 ClanRefusal::NotFound,
742 format!("there is no clan {clan}"),
743 )
744 })?;
745 let Some(offer) = row.offer_of(repo).filter(|o| o.to == to).cloned() else {
746 return Err(refused(
747 clan,
748 ClanRefusal::NotFound,
749 format!("{clan}/{repo} is not offered to {to}"),
750 ));
751 };
752 row.offers.retain(|o| o.repo != repo);
753 doc.clans.insert(clan.to_owned(), row)?;
754 Ok(offer)
755 })
756 }
757
758 pub(super) fn forget_clan_member(doc: &mut AuthDoc, id: &str) -> Result<()> {
765 if doc.clan_index.is_empty() {
766 return Ok(());
767 }
768 for name in doc.clan_index.remove(id)?.unwrap_or_default() {
769 let Some(mut row) = doc.clans.get(&name)? else {
770 continue;
771 };
772 row.members.retain(|m| m.principal != id);
773 row.invites.retain(|i| i.principal != id);
774 if row.admins() == 0 {
775 if let Some(first) = row.members.iter_mut().min_by_key(|m| m.joined_at) {
776 first.admin = true;
777 }
778 }
779 doc.clans.insert(name, row)?;
780 }
781 Ok(())
782 }
783
784 fn clan_for_admin(doc: &AuthDoc, clan: &str, by: &str, what: &str) -> Result<Clan> {
786 let row = Self::visible_clan(doc, clan, by)?;
787 Self::refuse_gated(doc, clan, by)?;
788 if row.role_of(by) != Some(ClanRole::Admin) {
789 return Err(refused(
790 clan,
791 ClanRefusal::Denied,
792 format!("only clan {clan}'s admin may {what}; {by} is not its admin"),
793 ));
794 }
795 Ok(row)
796 }
797
798 fn visible_clan(doc: &AuthDoc, clan: &str, who: &str) -> Result<Clan> {
802 match doc.clans.get(clan)? {
803 Some(row) if row.role_of(who).is_some() => Ok(row),
804 _ => Err(refused(
805 clan,
806 ClanRefusal::NotFound,
807 format!("there is no clan {clan} you are in or invited to"),
808 )),
809 }
810 }
811
812 fn ordinary_account(doc: &AuthDoc, clan: &str, id: &str, what: &str) -> Result<()> {
816 let Some(principal) = doc.principals.get(id)? else {
817 return Err(refused(
818 clan,
819 ClanRefusal::NotFound,
820 format!("there is no account {id}"),
821 ));
822 };
823 if principal.kind != PrincipalKind::User {
824 return Err(refused(
825 clan,
826 ClanRefusal::Precondition,
827 format!(
828 "{id} is a {:?}, and only a person's own account may {what}",
829 principal.kind
830 ),
831 ));
832 }
833 if let Some(employer) = doc.employers.get(id)? {
834 return Err(refused(
835 clan,
836 ClanRefusal::Precondition,
837 format!(
838 "{id} is an employee of {employer}: an enterprise account's people may not {what} — \
839 their account admin's rules win"
840 ),
841 ));
842 }
843 Self::refuse_gated(doc, clan, id)
844 }
845
846 fn refuse_gated(doc: &AuthDoc, clan: &str, id: &str) -> Result<()> {
848 if Self::gated(doc, id)? {
849 return Err(refused(
850 clan,
851 ClanRefusal::Precondition,
852 format!("account {id} is locked"),
853 ));
854 }
855 Ok(())
856 }
857
858 fn fits_in_room(
863 doc: &AuthDoc,
864 clan: &str,
865 account: &str,
866 bytes: u64,
867 room: AccountRoom,
868 ) -> Result<()> {
869 let Some(cap) = room.cap else { return Ok(()) };
870 let committed = room
871 .stored
872 .saturating_add(Self::promised_by(doc, account)?)
873 .saturating_add(Self::contributed_in(doc, account, Some(clan))?);
874 let free = cap.saturating_sub(committed);
875 if bytes > free {
876 return Err(refused(
877 clan,
878 ClanRefusal::Precondition,
879 format!(
880 "{account} has {} of its own room unused, and {} is asked for clan {clan}: {} short. \
881 A clan's room is its members' own; add room to the account, or give less",
882 human_bytes(free),
883 human_bytes(bytes),
884 human_bytes(bytes - free)
885 ),
886 ));
887 }
888 Ok(())
889 }
890
891 fn above_floor(
894 row: &Clan,
895 clan: &str,
896 member: &str,
897 bytes: u64,
898 used: Option<u64>,
899 lapsed: &[String],
900 what: &str,
901 ) -> Result<()> {
902 let Some(used) = used else {
903 return Err(refused(
904 clan,
905 ClanRefusal::Precondition,
906 format!(
907 "what clan {clan} stores could not be measured, so {member} may not {what} now: a room \
908 that is in use is never given back unseen"
909 ),
910 ));
911 };
912 let floor = row.floor_in_force(member, used, lapsed);
913 if bytes < floor {
914 let own = row.member(member).map_or(0, |m| m.contribution_bytes);
915 return Err(refused(
916 clan,
917 ClanRefusal::Precondition,
918 format!(
919 "clan {clan} stores {} in its room of {}, so {member}'s {} is in use: it may go down to {} \
920 and no lower, and {member} may not {what} until the clan stores less",
921 human_bytes(used),
922 human_bytes(row.room_in_force(lapsed)),
923 human_bytes(own),
924 human_bytes(floor)
925 ),
926 ));
927 }
928 Ok(())
929 }
930
931 fn index_clan(doc: &mut AuthDoc, principal: &str, clan: &str) -> Result<()> {
932 let mut names = doc.clan_index.get(principal)?.unwrap_or_default();
933 if !names.iter().any(|n| n == clan) {
934 names.push(clan.to_owned());
935 names.sort();
936 }
937 doc.clan_index.insert(principal.to_owned(), names)?;
938 Ok(())
939 }
940
941 fn unindex_clan(doc: &mut AuthDoc, principal: &str, clan: &str) -> Result<()> {
942 let Some(mut names) = doc.clan_index.get(principal)? else {
943 return Ok(());
944 };
945 names.retain(|n| n != clan);
946 if names.is_empty() {
947 doc.clan_index.remove(principal)?;
948 } else {
949 doc.clan_index.insert(principal.to_owned(), names)?;
950 }
951 Ok(())
952 }
953}