| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "embed" |
| 6 | "errors" |
| 7 | "strconv" |
| 8 | "strings" |
| 9 | "time" |
| 10 | |
| 11 | "gorm.io/gorm" |
| 12 | |
| 13 | "amadan.net/rastrillo/rastrillo/auth" |
| 14 | "amadan.net/rastrillo/rastrillo/migrate" |
| 15 | "amadan.net/rastrillo/rastrillo/sessions" |
| 16 | |
| 17 | "amadan.net/rastrillo/abono" |
| 18 | ) |
| 19 | |
| 20 | // User is a person, known by the address they sign in with. A row is |
| 21 | // made the first time an address signs in by its emailed link, or pays |
| 22 | // (holder, below), whichever comes first. Either way the only way into |
| 23 | // it is a link sent to that address. |
| 24 | type User struct { |
| 25 | ID int64 |
| 26 | Email string `gorm:"uniqueIndex"` |
| 27 | CreatedAt time.Time |
| 28 | } |
| 29 | |
| 30 | // Models is every model the schema generator manages. |
| 31 | var Models = []any{&User{}} |
| 32 | |
| 33 | //go:embed migrations/*.sql |
| 34 | var migrationFS embed.FS |
| 35 | |
| 36 | // Schema is the example's own migrations. BootSchema is everything |
| 37 | // applied at boot: sessions, auth (whose backfill reads sessions), |
| 38 | // abono's tables, then the example's. |
| 39 | var ( |
| 40 | Schema = migrate.MustFromFS(migrationFS, "example") |
| 41 | BootSchema = migrate.Merge(sessions.Schema, auth.Schema, abono.Schema, Schema) |
| 42 | ) |
| 43 | |
| 44 | func subjectOf(id int64) string { return strconv.FormatInt(id, 10) } |
| 45 | |
| 46 | func normalEmail(s string) string { return strings.ToLower(strings.TrimSpace(s)) } |
| 47 | |
| 48 | // ensureUser is the user with this address, made if there is none, as |
| 49 | // a subject. g may be a transaction. |
| 50 | func ensureUser(ctx context.Context, g *gorm.DB, email string) (string, error) { |
| 51 | var u User |
| 52 | err := g.WithContext(ctx).Where("email = ?", email).Take(&u).Error |
| 53 | if errors.Is(err, gorm.ErrRecordNotFound) { |
| 54 | u = User{Email: email, CreatedAt: time.Now().UTC()} |
| 55 | err = g.WithContext(ctx).Create(&u).Error |
| 56 | } |
| 57 | if err != nil { |
| 58 | return "", err |
| 59 | } |
| 60 | return subjectOf(u.ID), nil |
| 61 | } |
| 62 | |
| 63 | // holder is abono's Holder: paying is a way in, so an address abono has |
| 64 | // no subject for gets a user. Nobody is signed in by it; whoever reads |
| 65 | // that address's mail can sign in later and finds the membership there. |
| 66 | // It runs inside abono's transaction, so it uses tx. |
| 67 | func holder(ctx context.Context, tx *gorm.DB, email, _ string) (string, error) { |
| 68 | email = normalEmail(email) |
| 69 | if email == "" { |
| 70 | return "", nil |
| 71 | } |
| 72 | return ensureUser(ctx, tx, email) |
| 73 | } |
| 74 | |
| 75 | // subjectForAddress is auth's SubjectFor: a verified address signs in |
| 76 | // as its user, made on first sign-in. |
| 77 | func subjectForAddress(g *gorm.DB) func(string) (string, error) { |
| 78 | return func(address string) (string, error) { |
| 79 | return ensureUser(context.Background(), g, normalEmail(address)) |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | // userByEmail is the subject of the person with this address, for the |
| 84 | // owner's free-membership form. It makes nobody. |
| 85 | func userByEmail(g *gorm.DB) func(context.Context, string) (string, bool) { |
| 86 | return func(ctx context.Context, email string) (string, bool) { |
| 87 | var u User |
| 88 | if err := g.WithContext(ctx).Where("email = ?", normalEmail(email)).Take(&u).Error; err != nil { |
| 89 | return "", false |
| 90 | } |
| 91 | return subjectOf(u.ID), true |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | // emailOf is the address of subject, "" when there is none. It is the |
| 96 | // owner's pages' Label too. |
| 97 | func emailOf(g *gorm.DB) func(context.Context, string) string { |
| 98 | return func(ctx context.Context, subject string) string { |
| 99 | id, err := strconv.ParseInt(subject, 10, 64) |
| 100 | if err != nil { |
| 101 | return "" |
| 102 | } |
| 103 | var u User |
| 104 | if err := g.WithContext(ctx).Where("id = ?", id).Take(&u).Error; err != nil { |
| 105 | return "" |
| 106 | } |
| 107 | return u.Email |
| 108 | } |
| 109 | } |
| 110 | |