rastrillo / idear Public

Clone
git clone https://amadan.net/rastrillo/idear

Plain git — no account needed to clone.

Download

Download this file

1package idear
2
3import "time"
4
5// Member is a row in the roster: one person, at one role, in one
6// instance.
7//
8// Subject is the join to the app's own identity: idear does not own
9// email and password, and Subject is the only identifier both the
10// password and keymail identity plugins produce (see
11// docs/superpowers/specs/2026-08-23-idear-design.md §4). Email and
12// Name are a display cache, not the source of truth for who someone
13// is.
14//
15// Removal is DeactivatedAt, never a delete — see Active.
16type Member struct {
17 ID int64
18 Subject string `gorm:"uniqueIndex"`
19 Email string `gorm:"index"`
20 Name string
21 Role Role `gorm:"not null;index"`
22 DeactivatedAt *time.Time
23 CreatedAt time.Time
24 UpdatedAt time.Time
25}
26
27// Active reports whether m exists and has not been deactivated. A nil
28// Member is never active, so callers can pass a lookup's zero value
29// straight in without a separate nil check.
30func (m *Member) Active() bool {
31 return m != nil && m.DeactivatedAt == nil
32}
33
34// TableName namespaces the table as idear_members, not GORM's default
35// members, so it cannot collide with an app's own table of that name.
36func (Member) TableName() string { return "idear_members" }
37
38// Invitation is an offer to join the roster at a given Role, redeemed
39// by the holder of the plaintext token whose SHA-256 digest is stored
40// in TokenHash. Only the digest is ever persisted — see token.go —
41// matching sessions, which holds nothing but token digests either.
42type Invitation struct {
43 ID int64
44 Email string `gorm:"index"`
45 Role Role `gorm:"not null"`
46 TokenHash string `gorm:"uniqueIndex"`
47 InvitedBy int64
48 CreatedAt time.Time
49 ExpiresAt time.Time
50 AcceptedAt *time.Time
51 RevokedAt *time.Time
52}
53
54// Pending reports whether i can still be redeemed as of now: not yet
55// accepted, not revoked, and not expired. All three must hold — a
56// single check standing for the other two would let a revoked or
57// expired invitation read as usable the moment the case that also
58// disqualifies it stops being exercised.
59func (i *Invitation) Pending(now time.Time) bool {
60 return i.AcceptedAt == nil && i.RevokedAt == nil && i.ExpiresAt.After(now)
61}
62
63// TableName namespaces the table as idear_invitations, not GORM's
64// default invitations, so it cannot collide with an app's own table
65// of that name.
66func (Invitation) TableName() string { return "idear_invitations" }
67