package signin import ( "context" "errors" "net/netip" "slices" "time" "tug/internal/service/identity" "tug/internal/service/password" "tug/internal/service/session" "tug/internal/storage" "tug/internal/transport/web" ) type account struct { s *session.Service id *identity.Service host string adminURL string primary string google bool portdistrict bool } var _ web.AccountBackend = account{} func (s *Signin) Account() web.AccountBackend { return s.account } func (s *Signin) Ender() web.SessionEnder { return s.ender } func (a account) principal(p web.Principal) session.Principal { return session.Principal{SessionID: p.SessionID, UserID: p.UserID, Username: string(p.Username), Host: a.host} } func (a account) Profile(ctx context.Context, p web.Principal) (web.AccountProfile, error) { c, err := a.id.Claims(ctx, p.UserID, true) if err != nil { return web.AccountProfile{}, accountError(err) } prof := web.AccountProfile{ Username: c.Username, DisplayName: c.DisplayName, Groups: c.Groups, GroupCount: len(c.Groups), Google: a.google, Portdistrict: a.portdistrict, } if a.google { link, err := a.s.GoogleLink(ctx, p.UserID) if err != nil { return web.AccountProfile{}, accountError(err) } if link != nil { prof.GoogleLink = &web.AccountGoogleLink{LinkedAt: link.LinkedAt} } } keys, err := a.s.SSHKeys(ctx, p.UserID) if err != nil { return web.AccountProfile{}, accountError(err) } prof.SSHKeys = webSSHKeys(keys) if a.primary != "" && slices.Contains(c.Groups, a.primary) { prof.Primary = a.primary } if p.Admin && a.adminURL != "" { prof.AdminURL = a.adminURL } return prof, nil } func (a account) ChangePassword(ctx context.Context, p web.Principal, current, next string) error { _, err := a.s.ChangePassword(ctx, a.principal(p), current, next) return accountError(err) } func (a account) Sessions(ctx context.Context, p web.Principal, page int) (web.AccountSessions, error) { rows, err := a.s.List(ctx, p.UserID) if err != nil { return web.AccountSessions{}, accountError(err) } var browser string for _, r := range rows { if r.ID == p.SessionID { browser = r.Browser } } out := web.AccountSessions{Total: len(rows)} start := (page - 1) * web.AccountSessionsPerPage if page < 1 || start >= len(rows) { return out, nil } for _, r := range rows[start:min(start+web.AccountSessionsPerPage, len(rows))] { out.Sessions = append(out.Sessions, web.AccountSession{ ID: r.ID, Host: r.AppDomain, Created: r.CreatedAt, Expires: r.ExpiresAt, ThisSession: r.ID == p.SessionID, ThisBrowser: browser != "" && r.Browser == browser, ClientID: r.ClientID, UserAgent: r.UserAgent, Address: addressText(r.Address), LastSeen: lastSeen(r.LastSeenAt), }) } return out, nil } func (a account) RevokeSession(ctx context.Context, p web.Principal, id string) error { return accountError(a.s.RevokeOwn(ctx, a.principal(p), id)) } func (a account) RevokeOtherSessions(ctx context.Context, p web.Principal) (int, error) { n, err := a.s.RevokeOthers(ctx, a.principal(p)) return int(n), accountError(err) } func (a account) SignOutBrowser(ctx context.Context, p web.Principal) (int, error) { revoked, err := a.s.RevokeBrowser(ctx, a.principal(p)) if errors.Is(err, session.ErrNoSession) { return 0, nil } return int(revoked.Sessions), accountError(err) } func (a account) UpdateDisplayName(ctx context.Context, p web.Principal, name string) error { return accountError(a.s.UpdateDisplayName(ctx, a.principal(p), name)) } var accountProblems = []struct { err error want error }{ {session.ErrPasswordRejected, web.AccountPasswordWrong}, {password.ErrTooShort, web.AccountPasswordShort}, {password.ErrTooLong, web.AccountPasswordLong}, {session.ErrPasswordChanged, web.AccountPasswordChanged}, {session.ErrDisplayNameInvalid, web.AccountDisplayNameInvalid}, {session.ErrNoSuchSession, web.ErrAccountNotFound}, {session.ErrNoSuchSSHKey, web.ErrAccountNotFound}, {session.ErrUnavailable, web.AccountBusy}, {session.ErrNoSession, web.ErrNoSession}, } func (a account) SSHKeys(ctx context.Context, p web.Principal) ([]web.SSHKey, error) { keys, err := a.s.SSHKeys(ctx, p.UserID) if err != nil { return nil, accountError(err) } return webSSHKeys(keys), nil } func (a account) AddSSHKey(ctx context.Context, p web.Principal, key, label string) (web.SSHKey, error) { added, err := a.s.AddSSHKey(ctx, a.principal(p), key, label) if err != nil { return web.SSHKey{}, accountError(err) } return webSSHKey(added), nil } func (a account) RemoveSSHKey(ctx context.Context, p web.Principal, keyID string) (web.SSHKey, error) { removed, err := a.s.RemoveSSHKey(ctx, a.principal(p), keyID) if err != nil { return web.SSHKey{}, accountError(err) } return webSSHKey(removed), nil } func webSSHKey(k storage.SSHKey) web.SSHKey { return web.SSHKey{ID: k.ID, Fingerprint: k.Fingerprint, Type: k.Type, Label: k.Label, AddedAt: k.AddedAt} } func webSSHKeys(keys []storage.SSHKey) []web.SSHKey { var out []web.SSHKey for _, k := range keys { out = append(out, webSSHKey(k)) } return out } func accountError(err error) error { if err == nil { return nil } for _, m := range accountProblems { if errors.Is(err, m.err) { return m.want } } return err } func addressText(a netip.Addr) string { if !a.IsValid() { return "" } return a.String() } func lastSeen(at *time.Time) time.Time { if at == nil { return time.Time{} } return *at }