diff --git a/internal/events/membership.go b/internal/events/membership.go new file mode 100644 index 0000000..aa21de5 --- /dev/null +++ b/internal/events/membership.go @@ -0,0 +1,33 @@ +package events + +import "time" + +func init() { + register[MembershipRequested]("membership.requested") + register[MembershipAccepted]("membership.accepted") + register[MembershipDenied]("membership.denied") + register[MembershipRevoked]("membership.revoked") +} + +type ( + MembershipRequested struct { + Message string `json:"message"` + } + + MembershipAccepted struct { + Message string `json:"message"` + } + + MembershipDenied struct { + Until time.Time `json:"until"` + Message string `json:"message"` + } + + MembershipRevoked struct { + Message string `json:"message"` + } + + MembershipEnded struct { + Message string `json:"message"` + } +) diff --git a/internal/handlers/account.go b/internal/handlers/account.go index 42b97b7..314f951 100644 --- a/internal/handlers/account.go +++ b/internal/handlers/account.go @@ -97,7 +97,7 @@ func (h account) Show(w http.ResponseWriter, r *http.Request) { } if slug == "" && slug != user.Slug { - http.Redirect(w, r, named.URLFor("account-profile", user.Callsign().Call), http.StatusSeeOther) + http.Redirect(w, r, named.URLFor("account-profile", user.Callsign(r.Context()).Call), http.StatusSeeOther) return } diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go index c503ff0..1be4055 100644 --- a/internal/handlers/settings.go +++ b/internal/handlers/settings.go @@ -255,6 +255,8 @@ func (h settings) SettingsSave(w http.ResponseWriter, r *http.Request) { return } default: + fmt.Println("settings handler calling save settings") + spew.Dump(settings) if err := services.Account.SaveSettings(ctx, account.ID, &settings); err != nil { ErrorHandler(err)(w, r) return diff --git a/internal/models/account.go b/internal/models/account.go index b67e6f6..3ffc8a9 100644 --- a/internal/models/account.go +++ b/internal/models/account.go @@ -147,15 +147,15 @@ func (u *Account) Emails() ([]*Email, error) { return Find[Email](context.Background(), ByAccount(u.ID)) } -func (m *Account) PrimaryEmail() (Email, error) { +func (m *Account) PrimaryEmail() Email { emails, err := m.Emails() if err != nil { - return Email{}, err + return Email{} } if len(emails) > 0 { - return *emails[0], nil + return *emails[0] } - return Email{}, errors.New("no primary email") + return Email{} } func (m *Account) Members(ctx context.Context) []*Membership { @@ -217,57 +217,27 @@ func FindAccountByCallsign(ctx context.Context, callsign string) (*Account, erro return FindOne[Account](ctx, ByCallsign(callsign)) } -func (u *Account) Callsigns() ([]Callsign, error) { - if len(u.callsigns) > 0 { - return u.callsigns, nil - } - var callsigns []Callsign - rows, err := global.dao.FindCallsignsForAccount(context.Background(), u.ID) - if err != nil { - return callsigns, err - } - for _, row := range rows { - callsign := Callsign{ - ID: row.ID, - Call: row.Callsign, - Expires: row.Expires.Time, - Status: row.Status, - Latitude: row.Latitude.Float64, - Longitude: row.Longitude.Float64, - Firstname: row.Firstname.String, - Middlename: row.Middlename.String, - Lastname: row.Lastname.String, - Suffix: row.Suffix.String, - Address: row.Address.String, - City: row.City.String, - State: row.State.String, - Zip: row.Zip.String, - Country: row.Country.String, - } - callsigns = append(callsigns, callsign) - } - u.callsigns = callsigns - - return callsigns, nil +func (u *Account) Callsigns(ctx context.Context) ([]*Callsign, error) { + return Find[Callsign](ctx, ByAccount(u.ID)) } -func (m *Account) Callsign() Callsign { - calls, err := m.Callsigns() +func (m *Account) Callsign(ctx context.Context) *Callsign { + calls, err := m.Callsigns(ctx) if err != nil { - return Callsign{} + return &Callsign{} } if len(calls) == 0 { - return Callsign{} + return &Callsign{} } return calls[0] } -func (m *Account) Location() (float64, float64) { +func (m *Account) Location(ctx context.Context) (float64, float64) { if m.Settings.LocationSettings.HasLocation() { return m.Settings.LocationSettings.Location() } - callsign := m.Callsign() + callsign := m.Callsign(ctx) return callsign.Latitude, callsign.Longitude } diff --git a/internal/models/account_finder.go b/internal/models/account_finder.go index 680834f..0230d85 100644 --- a/internal/models/account_finder.go +++ b/internal/models/account_finder.go @@ -5,6 +5,8 @@ import ( "encoding/json" "time" + "github.com/davecgh/go-spew/spew" + "github.com/ryanfaerman/netctl/internal/dao" "github.com/ryanfaerman/netctl/internal/models/finders" ) @@ -136,11 +138,14 @@ func (m Account) Find(ctx context.Context, queries finders.QuerySet) (any, error return nil, err } + spew.Dump(a.Settings) + a.Name = a.Settings.ProfileSettings.Name a.About = a.Settings.ProfileSettings.About + a.Slug = a.Settings.ProfileSettings.Slug if !a.Settings.LocationSettings.HasLocation() { - callsigns, err := a.Callsigns() + callsigns, err := a.Callsigns(ctx) if err == nil { if len(callsigns) > 0 { callsign := callsigns[0] @@ -150,13 +155,6 @@ func (m Account) Find(ctx context.Context, queries finders.QuerySet) (any, error } } - switch { - case queries.HasField("callsigns"): - if _, err := (&a).Callsigns(); err != nil { - return nil, err - } - } - found[i] = &a } diff --git a/internal/models/callsign.go b/internal/models/callsign.go index 4604848..9d8e954 100644 --- a/internal/models/callsign.go +++ b/internal/models/callsign.go @@ -1,7 +1,10 @@ package models import ( + "fmt" "time" + + "github.com/ryanfaerman/netctl/hamdb" ) type Callsign struct { @@ -26,3 +29,8 @@ type Callsign struct { CreatedAt time.Time UpdatedAt time.Time } + +func (m Callsign) LicenseClass() string { + fmt.Println(m.Class) + return hamdb.LicenseClass(m.Class).String() +} diff --git a/internal/models/callsign_finder.go b/internal/models/callsign_finder.go new file mode 100644 index 0000000..7f9e846 --- /dev/null +++ b/internal/models/callsign_finder.go @@ -0,0 +1,65 @@ +package models + +import ( + "context" + "time" + + "github.com/ryanfaerman/netctl/internal/dao" + "github.com/ryanfaerman/netctl/internal/models/finders" +) + +func (m Callsign) FindCacheKey() string { + return "callsigns" +} + +func (m Callsign) FindCacheDuration() time.Duration { + return 7 * 24 * time.Hour +} + +func (m Callsign) Find(ctx context.Context, queries finders.QuerySet) (any, error) { + var ( + raws []dao.Callsign + err error + found []*Callsign + ) + + switch { + default: + return nil, finders.ErrInvalidWhere + case queries.HasWhere("account_id"): + accountID, ok := finders.EnforceValue[int64](queries, "account_id") + if ok != nil { + return nil, ok + } + raws, err = global.dao.FindCallsignsForAccount(ctx, accountID) + + } + + if err != nil { + return nil, err + } + + found = make([]*Callsign, len(raws)) + for i, raw := range raws { + found[i] = &Callsign{ + ID: raw.ID, + Call: raw.Callsign, + Class: raw.Class, + Expires: raw.Expires.Time, + Status: raw.Status, + Latitude: raw.Latitude.Float64, + Longitude: raw.Longitude.Float64, + Firstname: raw.Firstname.String, + Middlename: raw.Middlename.String, + Lastname: raw.Lastname.String, + Suffix: raw.Suffix.String, + Address: raw.Address.String, + City: raw.City.String, + State: raw.State.String, + Zip: raw.Zip.String, + Country: raw.Country.String, + } + } + + return found, nil +} diff --git a/internal/models/event.go b/internal/models/event.go index 1225f98..fee1bc9 100644 --- a/internal/models/event.go +++ b/internal/models/event.go @@ -85,6 +85,7 @@ func FindEventsForCallsign(eventType string, callsign string) (EventStream, erro type RecoveredEvent struct { RegisteredFn string + SessionToken string Event Event ID int64 } @@ -107,6 +108,7 @@ func FindRecoverableEvents(ctx context.Context) ([]RecoveredEvent, error) { stream[i] = RecoveredEvent{ ID: raw.RecoveryID, RegisteredFn: raw.RegisteredFn, + SessionToken: raw.SessionToken, Event: Event{ ID: raw.ID, At: raw.Created, diff --git a/internal/models/membership.go b/internal/models/membership.go index e58b16c..0139242 100644 --- a/internal/models/membership.go +++ b/internal/models/membership.go @@ -9,14 +9,25 @@ import ( "github.com/ryanfaerman/netctl/internal/models/finders" ) +type MembershipStatus int + +const ( + MembershipStatusPending MembershipStatus = iota + MembershipStatusActive + MembershipStatusDenied + MembershipStatusBanned + MembershipStatusInactive +) + type Membership struct { + CreatedAt time.Time AccountID int64 TargetID int64 RoleID int64 - CreatedAt time.Time - ID int64 + ID int64 + Status MembershipStatus } func (m *Membership) Account(ctx context.Context) *Account { diff --git a/internal/services/account.go b/internal/services/account.go index 6f5b7b8..45103e3 100644 --- a/internal/services/account.go +++ b/internal/services/account.go @@ -188,12 +188,7 @@ func (s account) AvatarURL(ctx context.Context, slugs ...string) string { return "" } - email, err := account.PrimaryEmail() - if err != nil { - fmt.Println("error getting primary email", "error", err) - return "" - } - fmt.Println(email.Address) + email := account.PrimaryEmail() h := sha256.New() h.Write([]byte(strings.TrimSpace(strings.ToLower(email.Address)))) @@ -215,6 +210,8 @@ func (a account) SaveSettings(ctx context.Context, id int64, settings *models.Se if err != nil { return err } + fmt.Println("args") + spew.Dump(settings) if err := mergo.Merge(&account.Settings, settings, mergo.WithOverride); err != nil { return err @@ -223,6 +220,7 @@ func (a account) SaveSettings(ctx context.Context, id int64, settings *models.Se if err := Validation.Apply(account.Settings); err != nil { return err } + spew.Dump(account.Settings) data, err := json.Marshal(account.Settings) if err != nil { @@ -244,7 +242,7 @@ func (a account) SaveSettings(ctx context.Context, id int64, settings *models.Se } func (s account) Geolocation(ctx context.Context, m *models.Account) (float64, float64, error) { - call := m.Callsign() + call := m.Callsign(ctx) return call.Latitude, call.Longitude, nil } diff --git a/internal/services/event.go b/internal/services/event.go index 03216c9..146d5cc 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -40,7 +40,7 @@ func (e *event) Create(ctx context.Context, stream string, evt any) error { id, err := global.dao.CreateEvent(ctx, dao.CreateEventParams{ StreamID: stream, - AccountID: 1, + AccountID: Session.GetAccount(ctx).ID, EventType: fmt.Sprintf("%T", evt), EventData: string(d), }) @@ -62,7 +62,7 @@ func (e *event) Create(ctx context.Context, stream string, evt any) error { Event: j, } - go e.Publish(context.Background(), event) + go e.Publish(context.WithoutCancel(ctx), event) return nil } @@ -88,7 +88,7 @@ func (e *event) Publish(ctx context.Context, event models.Event) error { } wg := workgroup.New(5) // TODO: make this configurable - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) // TODO: make this configurable + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Minute) // TODO: make this configurable defer cancel() for _, sub := range e.subscribers[event.Name] { @@ -102,9 +102,11 @@ func (e *event) Publish(ctx context.Context, event models.Event) error { } l.Debug("creating recovery tombstone") + recoveryID, err := global.dao.CreateEventRecovery(ctx, dao.CreateEventRecoveryParams{ EventsID: event.ID, RegisteredFn: sub.name, + SessionToken: global.session.Token(ctx), }) if err != nil { l.Error("error creating recovery tombstone", "error", err) @@ -145,6 +147,14 @@ func (e *event) Recover(ctx context.Context) error { wg := workgroup.New(5) // TODO: make this configurable for _, recovered := range recovereds { + ctx, err := global.session.Load(ctx, recovered.SessionToken) + if err != nil { + // cannot load the session, so it must not exist, + // thus... maybe we just delete these events? or log it? + // really need to think of a good approach here, cause that info would get lost if they log out. + // maybe need to have logout not delete the actual token, just the data within. + return fmt.Errorf("unable to load session: %w", err) + } for _, sub := range e.subscribers[recovered.Event.Name] { l = l.With("subscriber", sub.name, "event", recovered.Event.Name) if recovered.RegisteredFn != sub.name { diff --git a/internal/services/memberships.go b/internal/services/memberships.go index 87ddeff..2248daf 100644 --- a/internal/services/memberships.go +++ b/internal/services/memberships.go @@ -108,7 +108,6 @@ func (s membership) Create(ctx context.Context, owner, m *models.Account, email } callsignID = id m.Slug = license.Call - m.Settings.ProfileSettings.Slug = license.Call } id, err := qtx.CreateAccount(ctx, dao.CreateAccountParams{ @@ -120,6 +119,7 @@ func (s membership) Create(ctx context.Context, owner, m *models.Account, email } m.ID = id + m.Settings.ProfileSettings.Slug = m.Slug if err := Account.SaveSettings(ctx, m.ID, &m.Settings); err != nil { return err diff --git a/internal/services/net.go b/internal/services/net.go index ee78ec4..ed73d8b 100644 --- a/internal/services/net.go +++ b/internal/services/net.go @@ -101,6 +101,7 @@ func (n net) ValidateCheckinHeard(ctx context.Context, event models.Event) error } func (n net) ValidateCheckin(ctx context.Context, stream string, checkin *models.NetCheckin) error { + // return errors.New("break on purpose") license, err := hamdb.Lookup(ctx, checkin.Callsign.AsHeard) if err != nil { if err == hamdb.ErrNotFound { diff --git a/internal/services/service.go b/internal/services/service.go index 4410a8e..f2858d5 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -97,6 +97,7 @@ func transaction(ctx context.Context, fn func(context.Context, *dao.Queries) err ) tx, ok = ctx.Value(ctxKeyTX).(*sql.Tx) if !ok { + fmt.Println("starting transaction") tx, err = global.db.BeginTx(ctx, nil) if err != nil { return err @@ -104,12 +105,14 @@ func transaction(ctx context.Context, fn func(context.Context, *dao.Queries) err ctx = context.WithValue(ctx, ctxKeyTX, tx) defer tx.Rollback() } + fmt.Println("are we ok?", ok) err = fn(ctx, global.dao.WithTx(tx)) if err != nil { return err } - if ok { + if !ok { + fmt.Println("committing") return tx.Commit() } return nil diff --git a/internal/services/session.go b/internal/services/session.go index 8ce2898..ff49b90 100644 --- a/internal/services/session.go +++ b/internal/services/session.go @@ -143,7 +143,6 @@ func (session) Verify(ctx context.Context, token string) error { func (session) Destroy(ctx context.Context) error { global.log.Info("session destroyed") global.session.Clear(ctx) - global.session.Destroy(ctx) return nil } diff --git a/internal/sql/migrations/0016_add_token_to_event_recovery.sql b/internal/sql/migrations/0016_add_token_to_event_recovery.sql new file mode 100644 index 0000000..dd91add --- /dev/null +++ b/internal/sql/migrations/0016_add_token_to_event_recovery.sql @@ -0,0 +1,13 @@ +-- add_token_to_event_recovery +-- +goose Up +-- +goose StatementBegin +ALTER TABLE events_recovery +ADD COLUMN session_token text NOT NULL DEFAULT ''; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE events_recovery +DROP COLUMN session_token; +-- +goose StatementEnd + diff --git a/internal/sql/queries/accounts.sql b/internal/sql/queries/accounts.sql index 90c76b2..6589859 100644 --- a/internal/sql/queries/accounts.sql +++ b/internal/sql/queries/accounts.sql @@ -10,7 +10,7 @@ LIMIT 1; -- name: GetAccountBySlug :one SELECT accounts.* FROM accounts -WHERE UPPER(slug) = UPPER(@slug) +WHERE UPPER(settings->>'$.profile.slug') = UPPER(@slug) LIMIT 1; diff --git a/internal/sql/queries/events.sql b/internal/sql/queries/events.sql index d15f9ee..939fadb 100644 --- a/internal/sql/queries/events.sql +++ b/internal/sql/queries/events.sql @@ -22,6 +22,7 @@ where id IN (sqlc.slice('ids')); SELECT events_recovery.id as recovery_id, events_recovery.registered_fn as registered_fn, + events_recovery.session_token as session_token, events.* FROM events_recovery JOIN events ON events.id = events_recovery.events_id; @@ -29,9 +30,10 @@ JOIN events ON events.id = events_recovery.events_id; -- name: CreateEventRecovery :one INSERT INTO events_recovery ( events_id, - registered_fn + registered_fn, + session_token ) VALUES ( - ?1, ?2 + ?1, ?2, ?3 ) RETURNING id; -- name: DeleteEventRecovery :exec diff --git a/internal/views/account.templ b/internal/views/account.templ index 00fbf04..7cd71aa 100644 --- a/internal/views/account.templ +++ b/internal/views/account.templ @@ -17,7 +17,7 @@ type Account struct { func (v Account) Nav() Menu { return Menu{ - {Value: "Profile", URL: named.URLFor("account-profile", v.Account.Callsign().Call), Icon: IconAttrs{Name: "file-lines"}}, + {Value: "Profile", URL: named.URLFor("account-profile", v.Account.Callsign(context.Background()).Call), Icon: IconAttrs{Name: "file-lines"}}, } } @@ -35,7 +35,7 @@ templ (v Account) Profile() { }, })

{ v.Account.Name }

-

{ v.Account.Callsign().Call }

+

{ v.Account.Callsign(ctx).Call }

@Can("edit", v.Account) { if CurrentAccount(ctx) != v.Account { @@ -50,18 +50,20 @@ templ (v Account) Profile() {
@@ -97,7 +99,7 @@ templ (v Account) Profile() { }, }) -
{ m.Account(ctx).Callsign().Call }
+
{ m.Account(ctx).Callsign(ctx).Call }
} diff --git a/internal/views/navigation.templ b/internal/views/navigation.templ index 4f61612..cd51fdf 100644 --- a/internal/views/navigation.templ +++ b/internal/views/navigation.templ @@ -230,7 +230,7 @@ templ RightNavHeader() { }) { CurrentAccount(ctx).Name } - { CurrentAccount(ctx).Callsign().Call } + { CurrentAccount(ctx).Callsign(ctx).Call } } func RightNav(ctx context.Context) SideMenu { diff --git a/internal/views/settings.templ b/internal/views/settings.templ index 1bef423..614eae1 100644 --- a/internal/views/settings.templ +++ b/internal/views/settings.templ @@ -15,7 +15,7 @@ type Settings struct { func (v Settings) TopNav() Menu { return Menu{ - {Value: "Profile", URL: named.URLFor("account-profile", v.Account.Callsign().Call), Icon: IconAttrs{Name: "file-lines"}}, + {Value: "Profile", URL: named.URLFor("account-profile", v.Account.Callsign(context.Background()).Call), Icon: IconAttrs{Name: "file-lines"}}, } }