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}