Raw
  1package web
  2
  3import (
  4	"context"
  5	"errors"
  6	"log/slog"
  7	"net/http"
  8
  9	"tug/internal/transport/web/text"
 10)
 11
 12type ResetRedeemer interface {
 13	CheckReset(ctx context.Context, token string) (username string, err error)
 14	RedeemReset(ctx context.Context, token, password, confirm string) (username string, err error)
 15}
 16
 17var ErrResetInvalid = errors.New("web: reset link is not valid")
 18
 19const (
 20	ResetPath     = InvitePath + "reset/"
 21	ResetDonePath = ResetPath + "done"
 22)
 23
 24type resetData struct {
 25	Action   string
 26	Username Ident
 27	Message  text.Key
 28}
 29
 30type reset struct {
 31	kit      *Kit
 32	redeemer ResetRedeemer
 33}
 34
 35func (k *Kit) ResetRoutes(r ResetRedeemer) []Route {
 36	if r == nil {
 37		panic("web: the reset pages need a redeemer")
 38	}
 39	rs := &reset{kit: k, redeemer: r}
 40	return []Route{
 41		{Method: http.MethodGet, Path: ResetPath, Prefix: true, Access: Public, Handler: http.HandlerFunc(rs.show)},
 42		{Method: http.MethodPost, Path: ResetPath, Prefix: true, Access: Public, Handler: http.HandlerFunc(rs.submit), csrfFallback: k.endedSessionForm(rs.refreshed)},
 43		{Method: http.MethodGet, Path: ResetDonePath, Access: Public, Handler: http.HandlerFunc(rs.done)},
 44	}
 45}
 46
 47func (rs *reset) invalid(w http.ResponseWriter, r *http.Request) {
 48	rs.kit.renderError(w, r, http.StatusNotFound, text.ResetInvalidTitle, text.ResetInvalid)
 49}
 50
 51func (rs *reset) failed(w http.ResponseWriter, r *http.Request, step string, err error) {
 52	slog.Error("reset step failed", "step", step, "err", err)
 53	rs.kit.renderError(w, r, http.StatusInternalServerError, text.ErrorInternalTitle, text.ErrorInternal)
 54}
 55
 56func (rs *reset) render(w http.ResponseWriter, r *http.Request, status int, token string, d resetData) {
 57	d.Action = ResetPath + token
 58	rs.kit.render(w, r, status, "reset", rs.kit.formPage(w, r, d))
 59}
 60
 61func (rs *reset) check(w http.ResponseWriter, r *http.Request) (string, string, bool) {
 62	token, ok := inviteToken(r)
 63	if !ok {
 64		rs.invalid(w, r)
 65		return "", "", false
 66	}
 67	username, err := rs.redeemer.CheckReset(r.Context(), token)
 68	switch {
 69	case errors.Is(err, ErrResetInvalid):
 70		rs.invalid(w, r)
 71	case err != nil:
 72		rs.failed(w, r, "check", err)
 73	default:
 74		return token, username, true
 75	}
 76	return "", "", false
 77}
 78
 79func (rs *reset) show(w http.ResponseWriter, r *http.Request) {
 80	rs.again(w, r, http.StatusOK, "")
 81}
 82
 83func (rs *reset) refreshed(w http.ResponseWriter, r *http.Request) {
 84	rs.again(w, r, http.StatusOK, text.FormRefreshed)
 85}
 86
 87func (rs *reset) again(w http.ResponseWriter, r *http.Request, status int, message text.Key) {
 88	if token, username, ok := rs.check(w, r); ok {
 89		rs.render(w, r, status, token, resetData{Username: Ident(username), Message: message})
 90	}
 91}
 92
 93func (rs *reset) submit(w http.ResponseWriter, r *http.Request) {
 94	token, ok := inviteToken(r)
 95	if !ok {
 96		rs.invalid(w, r)
 97		return
 98	}
 99	username, err := rs.redeemer.RedeemReset(r.Context(), token, r.PostFormValue("password"), r.PostFormValue("confirm"))
100	var p InviteProblem
101	switch {
102	case err == nil:
103	case errors.Is(err, ErrResetInvalid):
104		rs.invalid(w, r)
105		return
106	case errors.As(err, &p) && inviteProblems[p].key != "":
107		rs.again(w, r, http.StatusOK, inviteProblems[p].key)
108		return
109	case errors.Is(err, ErrInviteBusy):
110		rs.again(w, r, http.StatusServiceUnavailable, text.ErrorBusy)
111		return
112	default:
113		rs.failed(w, r, "redemption", err)
114		return
115	}
116	if err := rs.kit.SetFlash(w, text.FlashResetDone, Ident(username)); err != nil {
117		slog.Error("reset flash not set", "err", err)
118	}
119	rs.kit.SeeOther(w, r, ResetDonePath)
120}
121
122func (rs *reset) done(w http.ResponseWriter, r *http.Request) {
123	rs.kit.render(w, r, http.StatusOK, "reset-done", rs.kit.newPage(w, r, nil))
124}