| 1 | package sheets |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | |
| 6 | "gorm.io/gorm" |
| 7 | ) |
| 8 | |
| 9 | // errLastOwner refuses the change that would leave a sheet with nobody |
| 10 | // who can share it. |
| 11 | // |
| 12 | // A sheet with no owner cannot be shared, cannot be un-shared, and |
| 13 | // cannot be given an owner — mayShare is false for everybody, including |
| 14 | // the person who did it, and there is no admin path to repair it. That |
| 15 | // makes it the one state in this package worth an explicit sentinel |
| 16 | // rather than a boolean: both doors have to refuse it, and both have to |
| 17 | // say the same thing to the person. |
| 18 | // |
| 19 | // The doors are revoke and grant. Only revoke was guarded at first; |
| 20 | // grant upserts on (sheet_id, member_id), so re-sharing with an address |
| 21 | // that already had access changed their role and counted nothing. The |
| 22 | // sole owner picking "viewer" for their own address orphaned the sheet |
| 23 | // in one form submission. |
| 24 | var errLastOwner = errors.New("sheets: a sheet needs an owner") |
| 25 | |
| 26 | // lastOwnerGoing reports whether changing this member's role away from |
| 27 | // owner would take the sheet's last owner with it. |
| 28 | // |
| 29 | // It runs inside the caller's transaction, and that is the point rather |
| 30 | // than tidiness: read-then-write outside one is check-then-act, and two |
| 31 | // owners demoting each other concurrently both counted two, both passed, |
| 32 | // and both wrote. SQLite serialises the writes; it does not serialise a |
| 33 | // read against someone else's later write. |
| 34 | func lastOwnerGoing(tx *gorm.DB, sheetID, memberID uint64, newRole SheetRole) (bool, error) { |
| 35 | if newRole == SheetOwner { |
| 36 | return false, nil // not going anywhere |
| 37 | } |
| 38 | var current Grant |
| 39 | err := tx.Where("sheet_id = ? AND member_id = ?", sheetID, memberID). |
| 40 | First(¤t).Error |
| 41 | if errors.Is(err, gorm.ErrRecordNotFound) { |
| 42 | return false, nil // holds nothing, so takes nothing with them |
| 43 | } |
| 44 | if err != nil { |
| 45 | return false, err |
| 46 | } |
| 47 | if SheetRole(current.Role) != SheetOwner { |
| 48 | return false, nil |
| 49 | } |
| 50 | var owners int64 |
| 51 | if err := tx.Model(&Grant{}). |
| 52 | Where("sheet_id = ? AND role = ?", sheetID, string(SheetOwner)). |
| 53 | Count(&owners).Error; err != nil { |
| 54 | return false, err |
| 55 | } |
| 56 | return owners <= 1, nil |
| 57 | } |
| 58 | |