Raw
  1package signin
  2
  3import (
  4	"context"
  5	"errors"
  6	"net/netip"
  7	"slices"
  8	"time"
  9
 10	"tug/internal/service/identity"
 11	"tug/internal/service/password"
 12	"tug/internal/service/session"
 13	"tug/internal/storage"
 14	"tug/internal/transport/web"
 15)
 16
 17type account struct {
 18	s            *session.Service
 19	id           *identity.Service
 20	host         string
 21	adminURL     string
 22	primary      string
 23	google       bool
 24	portdistrict bool
 25}
 26
 27var _ web.AccountBackend = account{}
 28
 29func (s *Signin) Account() web.AccountBackend {
 30	return s.account
 31}
 32
 33func (s *Signin) Ender() web.SessionEnder {
 34	return s.ender
 35}
 36
 37func (a account) principal(p web.Principal) session.Principal {
 38	return session.Principal{SessionID: p.SessionID, UserID: p.UserID, Username: string(p.Username), Host: a.host}
 39}
 40
 41func (a account) Profile(ctx context.Context, p web.Principal) (web.AccountProfile, error) {
 42	c, err := a.id.Claims(ctx, p.UserID, true)
 43	if err != nil {
 44		return web.AccountProfile{}, accountError(err)
 45	}
 46	prof := web.AccountProfile{
 47		Username:     c.Username,
 48		DisplayName:  c.DisplayName,
 49		Groups:       c.Groups,
 50		GroupCount:   len(c.Groups),
 51		Google:       a.google,
 52		Portdistrict: a.portdistrict,
 53	}
 54	if a.google {
 55		link, err := a.s.GoogleLink(ctx, p.UserID)
 56		if err != nil {
 57			return web.AccountProfile{}, accountError(err)
 58		}
 59		if link != nil {
 60			prof.GoogleLink = &web.AccountGoogleLink{LinkedAt: link.LinkedAt}
 61		}
 62	}
 63	keys, err := a.s.SSHKeys(ctx, p.UserID)
 64	if err != nil {
 65		return web.AccountProfile{}, accountError(err)
 66	}
 67	prof.SSHKeys = webSSHKeys(keys)
 68	if a.primary != "" && slices.Contains(c.Groups, a.primary) {
 69		prof.Primary = a.primary
 70	}
 71	if p.Admin && a.adminURL != "" {
 72		prof.AdminURL = a.adminURL
 73	}
 74	return prof, nil
 75}
 76
 77func (a account) ChangePassword(ctx context.Context, p web.Principal, current, next string) error {
 78	_, err := a.s.ChangePassword(ctx, a.principal(p), current, next)
 79	return accountError(err)
 80}
 81
 82func (a account) Sessions(ctx context.Context, p web.Principal, page int) (web.AccountSessions, error) {
 83	rows, err := a.s.List(ctx, p.UserID)
 84	if err != nil {
 85		return web.AccountSessions{}, accountError(err)
 86	}
 87	var browser string
 88	for _, r := range rows {
 89		if r.ID == p.SessionID {
 90			browser = r.Browser
 91		}
 92	}
 93	out := web.AccountSessions{Total: len(rows)}
 94	start := (page - 1) * web.AccountSessionsPerPage
 95	if page < 1 || start >= len(rows) {
 96		return out, nil
 97	}
 98	for _, r := range rows[start:min(start+web.AccountSessionsPerPage, len(rows))] {
 99		out.Sessions = append(out.Sessions, web.AccountSession{
100			ID:          r.ID,
101			Host:        r.AppDomain,
102			Created:     r.CreatedAt,
103			Expires:     r.ExpiresAt,
104			ThisSession: r.ID == p.SessionID,
105			ThisBrowser: browser != "" && r.Browser == browser,
106			ClientID:    r.ClientID,
107			UserAgent:   r.UserAgent,
108			Address:     addressText(r.Address),
109			LastSeen:    lastSeen(r.LastSeenAt),
110		})
111	}
112	return out, nil
113}
114
115func (a account) RevokeSession(ctx context.Context, p web.Principal, id string) error {
116	return accountError(a.s.RevokeOwn(ctx, a.principal(p), id))
117}
118
119func (a account) RevokeOtherSessions(ctx context.Context, p web.Principal) (int, error) {
120	n, err := a.s.RevokeOthers(ctx, a.principal(p))
121	return int(n), accountError(err)
122}
123
124func (a account) SignOutBrowser(ctx context.Context, p web.Principal) (int, error) {
125	revoked, err := a.s.RevokeBrowser(ctx, a.principal(p))
126	if errors.Is(err, session.ErrNoSession) {
127		return 0, nil
128	}
129	return int(revoked.Sessions), accountError(err)
130}
131
132func (a account) UpdateDisplayName(ctx context.Context, p web.Principal, name string) error {
133	return accountError(a.s.UpdateDisplayName(ctx, a.principal(p), name))
134}
135
136var accountProblems = []struct {
137	err  error
138	want error
139}{
140	{session.ErrPasswordRejected, web.AccountPasswordWrong},
141	{password.ErrTooShort, web.AccountPasswordShort},
142	{password.ErrTooLong, web.AccountPasswordLong},
143	{session.ErrPasswordChanged, web.AccountPasswordChanged},
144	{session.ErrDisplayNameInvalid, web.AccountDisplayNameInvalid},
145	{session.ErrNoSuchSession, web.ErrAccountNotFound},
146	{session.ErrNoSuchSSHKey, web.ErrAccountNotFound},
147	{session.ErrUnavailable, web.AccountBusy},
148	{session.ErrNoSession, web.ErrNoSession},
149}
150
151func (a account) SSHKeys(ctx context.Context, p web.Principal) ([]web.SSHKey, error) {
152	keys, err := a.s.SSHKeys(ctx, p.UserID)
153	if err != nil {
154		return nil, accountError(err)
155	}
156	return webSSHKeys(keys), nil
157}
158
159func (a account) AddSSHKey(ctx context.Context, p web.Principal, key, label string) (web.SSHKey, error) {
160	added, err := a.s.AddSSHKey(ctx, a.principal(p), key, label)
161	if err != nil {
162		return web.SSHKey{}, accountError(err)
163	}
164	return webSSHKey(added), nil
165}
166
167func (a account) RemoveSSHKey(ctx context.Context, p web.Principal, keyID string) (web.SSHKey, error) {
168	removed, err := a.s.RemoveSSHKey(ctx, a.principal(p), keyID)
169	if err != nil {
170		return web.SSHKey{}, accountError(err)
171	}
172	return webSSHKey(removed), nil
173}
174
175func webSSHKey(k storage.SSHKey) web.SSHKey {
176	return web.SSHKey{ID: k.ID, Fingerprint: k.Fingerprint, Type: k.Type, Label: k.Label, AddedAt: k.AddedAt}
177}
178
179func webSSHKeys(keys []storage.SSHKey) []web.SSHKey {
180	var out []web.SSHKey
181	for _, k := range keys {
182		out = append(out, webSSHKey(k))
183	}
184	return out
185}
186
187func accountError(err error) error {
188	if err == nil {
189		return nil
190	}
191	for _, m := range accountProblems {
192		if errors.Is(err, m.err) {
193			return m.want
194		}
195	}
196	return err
197}
198
199func addressText(a netip.Addr) string {
200	if !a.IsValid() {
201		return ""
202	}
203	return a.String()
204}
205
206func lastSeen(at *time.Time) time.Time {
207	if at == nil {
208		return time.Time{}
209	}
210	return *at
211}