package web import ( "context" "errors" "log/slog" "net/http" "tug/internal/transport/web/text" ) type ResetRedeemer interface { CheckReset(ctx context.Context, token string) (username string, err error) RedeemReset(ctx context.Context, token, password, confirm string) (username string, err error) } var ErrResetInvalid = errors.New("web: reset link is not valid") const ( ResetPath = InvitePath + "reset/" ResetDonePath = ResetPath + "done" ) type resetData struct { Action string Username Ident Message text.Key } type reset struct { kit *Kit redeemer ResetRedeemer } func (k *Kit) ResetRoutes(r ResetRedeemer) []Route { if r == nil { panic("web: the reset pages need a redeemer") } rs := &reset{kit: k, redeemer: r} return []Route{ {Method: http.MethodGet, Path: ResetPath, Prefix: true, Access: Public, Handler: http.HandlerFunc(rs.show)}, {Method: http.MethodPost, Path: ResetPath, Prefix: true, Access: Public, Handler: http.HandlerFunc(rs.submit), csrfFallback: k.endedSessionForm(rs.refreshed)}, {Method: http.MethodGet, Path: ResetDonePath, Access: Public, Handler: http.HandlerFunc(rs.done)}, } } func (rs *reset) invalid(w http.ResponseWriter, r *http.Request) { rs.kit.renderError(w, r, http.StatusNotFound, text.ResetInvalidTitle, text.ResetInvalid) } func (rs *reset) failed(w http.ResponseWriter, r *http.Request, step string, err error) { slog.Error("reset step failed", "step", step, "err", err) rs.kit.renderError(w, r, http.StatusInternalServerError, text.ErrorInternalTitle, text.ErrorInternal) } func (rs *reset) render(w http.ResponseWriter, r *http.Request, status int, token string, d resetData) { d.Action = ResetPath + token rs.kit.render(w, r, status, "reset", rs.kit.formPage(w, r, d)) } func (rs *reset) check(w http.ResponseWriter, r *http.Request) (string, string, bool) { token, ok := inviteToken(r) if !ok { rs.invalid(w, r) return "", "", false } username, err := rs.redeemer.CheckReset(r.Context(), token) switch { case errors.Is(err, ErrResetInvalid): rs.invalid(w, r) case err != nil: rs.failed(w, r, "check", err) default: return token, username, true } return "", "", false } func (rs *reset) show(w http.ResponseWriter, r *http.Request) { rs.again(w, r, http.StatusOK, "") } func (rs *reset) refreshed(w http.ResponseWriter, r *http.Request) { rs.again(w, r, http.StatusOK, text.FormRefreshed) } func (rs *reset) again(w http.ResponseWriter, r *http.Request, status int, message text.Key) { if token, username, ok := rs.check(w, r); ok { rs.render(w, r, status, token, resetData{Username: Ident(username), Message: message}) } } func (rs *reset) submit(w http.ResponseWriter, r *http.Request) { token, ok := inviteToken(r) if !ok { rs.invalid(w, r) return } username, err := rs.redeemer.RedeemReset(r.Context(), token, r.PostFormValue("password"), r.PostFormValue("confirm")) var p InviteProblem switch { case err == nil: case errors.Is(err, ErrResetInvalid): rs.invalid(w, r) return case errors.As(err, &p) && inviteProblems[p].key != "": rs.again(w, r, http.StatusOK, inviteProblems[p].key) return case errors.Is(err, ErrInviteBusy): rs.again(w, r, http.StatusServiceUnavailable, text.ErrorBusy) return default: rs.failed(w, r, "redemption", err) return } if err := rs.kit.SetFlash(w, text.FlashResetDone, Ident(username)); err != nil { slog.Error("reset flash not set", "err", err) } rs.kit.SeeOther(w, r, ResetDonePath) } func (rs *reset) done(w http.ResponseWriter, r *http.Request) { rs.kit.render(w, r, http.StatusOK, "reset-done", rs.kit.newPage(w, r, nil)) }