1package invite
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "time"
8
9 "tug/internal/service/password"
10 "tug/internal/storage"
11)
12
13const ResetLifetime = 24 * time.Hour
14
15const resetSegment = "reset/"
16
17var ErrResetInvalid = errors.New("invite: reset link is not valid")
18
19type IssuedReset struct {
20 URL string
21 Username string
22 ExpiresAt time.Time
23}
24
25func (s *Service) IssueReset(ctx context.Context, actor, userID string) (IssuedReset, error) {
26 token, hash, err := newToken()
27 if err != nil {
28 return IssuedReset{}, err
29 }
30 issued, err := s.store.IssueResetLinkAudited(ctx, userID, hash, ResetLifetime, actor)
31 if err != nil {
32 return IssuedReset{}, fmt.Errorf("invite: issue reset link: %w", err)
33 }
34 return IssuedReset{URL: s.url(resetSegment + token), Username: issued.Username, ExpiresAt: issued.ExpiresAt}, nil
35}
36
37func (s *Service) CheckReset(ctx context.Context, token string) (string, error) {
38 hash, ok := tokenHash(token)
39 if !ok {
40 return "", ErrResetInvalid
41 }
42 target, err := s.store.CheckResetLink(ctx, hash)
43 if err != nil {
44 return "", resetError(err)
45 }
46 return target.Username, nil
47}
48
49func (s *Service) RedeemReset(ctx context.Context, token, newPassword, confirm string) (string, error) {
50 hash, ok := tokenHash(token)
51 if !ok {
52 return "", ErrResetInvalid
53 }
54 if _, err := s.store.CheckResetLink(ctx, hash); err != nil {
55 return "", resetError(err)
56 }
57 if err := password.Acceptable(newPassword); err != nil {
58 return "", err
59 }
60 if confirm != newPassword {
61 return "", ErrPasswordMismatch
62 }
63 passwordHash, err := password.HashContext(ctx, newPassword)
64 if err != nil {
65 return "", fmt.Errorf("invite: hash password: %w", err)
66 }
67 target, revoked, err := s.store.RedeemResetLinkAudited(ctx, hash, passwordHash)
68 if err != nil {
69 return "", resetError(err)
70 }
71 s.logger.Info("reset link redeemed", "username", target.Username, "sessions", revoked.Sessions)
72 return target.Username, nil
73}
74
75func resetError(err error) error {
76 if errors.Is(err, storage.ErrResetLinkInvalid) {
77 return ErrResetInvalid
78 }
79 return fmt.Errorf("invite: %w", err)
80}