Raw
 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}