sign-in: in-app password reset (emailed code) on Mac/Linux and Windows; v0.3.4
- auth.PasswordForgot / auth.PasswordReset (+ httptest tests); ErrResetUnavailable on 404/401/405/5xx so dialogs offer https://customer.verae.ai/portal/ - keyflow_gui: 'Forgot password?' on the password step and after a failed sign-in - Windows: Forgot password? link, forgot/reset/unavailable forms - bump HelperVersion, deb and pkg default VERSION to 0.3.4
This commit is contained in:
parent
f8e74b56db
commit
179c974b60
7 changed files with 468 additions and 7 deletions
|
|
@ -45,6 +45,8 @@ function Btn([string]$text, [int]$x, [int]$w, [string]$res) {
|
|||
switch ($kind) {
|
||||
'login' {
|
||||
$e = Field 'Email' $false $pre; $p = Field 'Password' $true ''; $t = Field '2FA code (if enabled)' $false ''
|
||||
$fl = New-Object Windows.Forms.LinkLabel; $fl.Text = 'Forgot password?'; $fl.Left = 170; $fl.Top = $script:y; $fl.Width = 200
|
||||
$fl.Add_LinkClicked({ $script:result = 'forgot'; $f.Close() }); $f.Controls.Add($fl); $script:y += 24
|
||||
$script:y += 8
|
||||
$ok = Btn 'Sign in' 15 100 'login'; $f.AcceptButton = $ok
|
||||
Btn 'Create account' 125 120 'create' | Out-Null
|
||||
|
|
@ -53,7 +55,35 @@ switch ($kind) {
|
|||
$f.Height = $script:y + 75
|
||||
if ($pre) { $f.Add_Shown({ $p.Focus() }) }
|
||||
[void]$f.ShowDialog()
|
||||
if ($script:result -eq 'login') { "login|$(B $e.Text)|$(B $p.Text)|$(B $t.Text)" } else { $script:result }
|
||||
if ($script:result -eq 'login') { "login|$(B $e.Text)|$(B $p.Text)|$(B $t.Text)" }
|
||||
elseif ($script:result -eq 'forgot') { "forgot|$(B $e.Text)" } else { $script:result }
|
||||
}
|
||||
'forgot' {
|
||||
$e = Field 'Email' $false $pre
|
||||
$script:y += 8
|
||||
$ok = Btn 'Send code' 15 100 'send'; $f.AcceptButton = $ok
|
||||
$c = Btn 'Back' 410 100 'cancel'; $f.CancelButton = $c
|
||||
$f.Height = $script:y + 75
|
||||
[void]$f.ShowDialog()
|
||||
if ($script:result -eq 'send') { "send|$(B $e.Text)" } else { 'cancel' }
|
||||
}
|
||||
'reset' {
|
||||
$k = Field 'Code from the email' $false ''; $p = Field 'New password' $true ''; $p2 = Field 'Confirm new password' $true ''
|
||||
$script:y += 8
|
||||
$ok = Btn 'Reset password' 15 130 'reset'; $f.AcceptButton = $ok
|
||||
Btn 'Send a new code' 155 130 'resend' | Out-Null
|
||||
$c = Btn 'Cancel' 410 100 'cancel'; $f.CancelButton = $c
|
||||
$f.Height = $script:y + 75
|
||||
[void]$f.ShowDialog()
|
||||
if ($script:result -eq 'reset') { "reset|$(B $k.Text)|$(B $p.Text)|$(B $p2.Text)" } else { $script:result }
|
||||
}
|
||||
'unavail' {
|
||||
$ok = Btn 'Open customer.verae.ai' 15 200 'open'; $f.AcceptButton = $ok
|
||||
$c = Btn 'Back' 410 100 'cancel'; $f.CancelButton = $c
|
||||
$f.Height = $script:y + 75
|
||||
[void]$f.ShowDialog()
|
||||
if ($script:result -eq 'open') { Start-Process 'https://customer.verae.ai/portal/' }
|
||||
'cancel'
|
||||
}
|
||||
'create' {
|
||||
$n = Field 'Your name' $false ''; $e = Field 'Email' $false $pre; $p = Field 'Password' $true ''; $p2 = Field 'Confirm password' $true ''
|
||||
|
|
@ -143,9 +173,21 @@ func acquireKeyInteractive(reason string) bool {
|
|||
}
|
||||
if err != nil {
|
||||
vlog.Errorf("sign-in: login failed for %s: %v", auth.MaskEmail(email), err)
|
||||
msg = "Sign-in failed: " + err.Error()
|
||||
msg = "Sign-in failed: " + err.Error() + "\r\nForgot your password? Click \"Forgot password?\" below."
|
||||
continue
|
||||
}
|
||||
case "forgot":
|
||||
if e := strings.TrimSpace(f[0]); e != "" {
|
||||
email = e
|
||||
}
|
||||
k, ok, m := winForgotFlow(caps, email)
|
||||
if m != "" {
|
||||
msg = m
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key = k
|
||||
case "create":
|
||||
k, ok := winCreateFlow(caps, email)
|
||||
if !ok {
|
||||
|
|
@ -231,6 +273,95 @@ func winCreateFlow(caps auth.Capabilities, email string) (string, bool) {
|
|||
return "", false
|
||||
}
|
||||
|
||||
// winResetUnavailable: in-app reset not supported; offer the web portal.
|
||||
func winResetUnavailable() {
|
||||
_, _, _ = winForm("unavail", "Password reset isn't available in the app yet.\r\n\r\nYou can reset your password on the Verae customer portal in your browser ("+auth.PortalResetURL+").", "")
|
||||
}
|
||||
|
||||
// winForgotFlow: emailed-code password reset. Returns (key, ok, message for the sign-in form).
|
||||
func winForgotFlow(caps auth.Capabilities, email string) (string, bool, string) {
|
||||
msg := "Reset your Verae password.\r\nWe'll email a 6-digit code to this address."
|
||||
for i := 0; i < 10; i++ {
|
||||
act, f, err := winForm("forgot", msg, email)
|
||||
if err != nil || act != "send" {
|
||||
return "", false, ""
|
||||
}
|
||||
email = strings.TrimSpace(f[0])
|
||||
if !auth.ValidEmail(email) {
|
||||
msg = "Please enter a valid email address."
|
||||
continue
|
||||
}
|
||||
vlog.Printf("sign-in: password reset requested for %s", auth.MaskEmail(email))
|
||||
if ferr := auth.PasswordForgot(email); ferr != nil {
|
||||
vlog.Errorf("sign-in: password forgot %s: %v", auth.MaskEmail(email), ferr)
|
||||
if errors.Is(ferr, auth.ErrResetUnavailable) {
|
||||
winResetUnavailable()
|
||||
return "", false, ""
|
||||
}
|
||||
msg = "Could not send a reset code: " + ferr.Error()
|
||||
continue
|
||||
}
|
||||
cmsg := "If " + email + " has a Verae account, we emailed it a 6-digit code (valid 15 minutes). Check your spam folder.\r\n" + caps.Policy.Describe()
|
||||
for j := 0; j < 15; j++ {
|
||||
act, cf, err := winForm("reset", cmsg, "")
|
||||
if err != nil || act == "cancel" || act == "" {
|
||||
return "", false, ""
|
||||
}
|
||||
if act == "resend" {
|
||||
if rerr := auth.PasswordForgot(email); rerr != nil {
|
||||
if errors.Is(rerr, auth.ErrResetUnavailable) {
|
||||
winResetUnavailable()
|
||||
return "", false, ""
|
||||
}
|
||||
cmsg = "Could not send a new code: " + rerr.Error()
|
||||
} else {
|
||||
cmsg = "A new code was sent to " + email + ".\r\n" + caps.Policy.Describe()
|
||||
}
|
||||
continue
|
||||
}
|
||||
code, pw := strings.TrimSpace(cf[0]), cf[1]
|
||||
switch {
|
||||
case len(code) != 6 || strings.Trim(code, "0123456789") != "":
|
||||
cmsg = "Enter the 6-digit code from the email.\r\n" + caps.Policy.Describe()
|
||||
continue
|
||||
case pw != cf[2]:
|
||||
cmsg = "The passwords do not match.\r\n" + caps.Policy.Describe()
|
||||
continue
|
||||
}
|
||||
if perr := caps.Policy.Check(pw, email); perr != nil {
|
||||
cmsg = perr.Error() + "\r\n" + caps.Policy.Describe()
|
||||
continue
|
||||
}
|
||||
key, rerr := auth.PasswordReset(email, code, pw, tokenName())
|
||||
cf[1], cf[2] = "", ""
|
||||
if rerr != nil {
|
||||
vlog.Errorf("sign-in: password reset %s: %v", auth.MaskEmail(email), rerr)
|
||||
if errors.Is(rerr, auth.ErrResetUnavailable) {
|
||||
winResetUnavailable()
|
||||
return "", false, ""
|
||||
}
|
||||
cmsg = "Could not reset the password: " + rerr.Error()
|
||||
continue
|
||||
}
|
||||
vlog.Printf("sign-in: password reset ok for %s (key returned=%v)", auth.MaskEmail(email), key != "")
|
||||
if key != "" {
|
||||
return key, true, ""
|
||||
}
|
||||
key, lerr := auth.InstallLogin(caps, email, pw, "", tokenName())
|
||||
pw = ""
|
||||
if lerr != nil {
|
||||
if errors.Is(lerr, auth.ErrTOTPRequired) {
|
||||
return "", false, "Your password was changed. Sign in with your new password and your 6-digit authenticator code."
|
||||
}
|
||||
return "", false, "Your password was changed, but signing in failed: " + lerr.Error() + "\r\nSign in with your new password."
|
||||
}
|
||||
return key, true, ""
|
||||
}
|
||||
return "", false, ""
|
||||
}
|
||||
return "", false, ""
|
||||
}
|
||||
|
||||
// pasteFlow: masked key field + portal button; validated before saving.
|
||||
func pasteFlow(reason string) bool {
|
||||
msg := "Open customer.verae.ai, sign in, open Security and create an xVerae- token (shown once). Paste it below."
|
||||
|
|
|
|||
|
|
@ -102,6 +102,8 @@ func keyFlow(reason string) (string, bool) {
|
|||
}
|
||||
}
|
||||
|
||||
const btnForgot = "Forgot password?"
|
||||
|
||||
func macLogin(caps auth.Capabilities) (string, error) {
|
||||
email := ""
|
||||
for {
|
||||
|
|
@ -114,10 +116,17 @@ func macLogin(caps auth.Capabilities) (string, error) {
|
|||
say("Please enter a valid email address.")
|
||||
continue
|
||||
}
|
||||
btn, pw, err := ask("Password for "+email+":", "", true, true, []string{"Back", "Sign in"}, "Sign in")
|
||||
btn, pw, err := ask("Password for "+email+":", "", true, true, []string{"Back", btnForgot, "Sign in"}, "Sign in")
|
||||
if err != nil || btn == "Back" {
|
||||
continue
|
||||
}
|
||||
if btn == btnForgot {
|
||||
key, ferr := macForgot(caps, email)
|
||||
if errors.Is(ferr, errCancel) {
|
||||
continue
|
||||
}
|
||||
return key, ferr
|
||||
}
|
||||
vlog.Printf("mac-key-setup: login %s", auth.MaskEmail(email))
|
||||
key, err := auth.InstallLogin(caps, email, pw, "", tokenName())
|
||||
if errors.Is(err, auth.ErrTOTPRequired) {
|
||||
|
|
@ -130,13 +139,128 @@ func macLogin(caps auth.Capabilities) (string, error) {
|
|||
pw = ""
|
||||
if err != nil {
|
||||
vlog.Errorf("mac-key-setup: login failed for %s: %v", auth.MaskEmail(email), err)
|
||||
say("Sign-in failed: " + err.Error())
|
||||
b, _, _ := ask("Sign-in failed: "+err.Error(), "", false, false, []string{btnForgot, "Try again"}, "Try again")
|
||||
if b == btnForgot {
|
||||
key, ferr := macForgot(caps, email)
|
||||
if errors.Is(ferr, errCancel) {
|
||||
continue
|
||||
}
|
||||
return key, ferr
|
||||
}
|
||||
continue
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
|
||||
// offerPortalReset tells the user in-app reset is unavailable and offers the web portal.
|
||||
func offerPortalReset() {
|
||||
b, _, _ := ask("Password reset isn't available in the app yet.\n\nYou can reset your password on the Verae customer portal in your browser ("+auth.PortalResetURL+").", "", false, false, []string{"Back", "Open portal"}, "Open portal")
|
||||
if b == "Open portal" {
|
||||
openURL(auth.PortalResetURL)
|
||||
}
|
||||
}
|
||||
|
||||
// macForgot: emailed-code password reset. Returns a minted key, or errCancel.
|
||||
func macForgot(caps auth.Capabilities, email string) (string, error) {
|
||||
for {
|
||||
btn, e, err := ask("Reset your Verae password\n\nWe'll email a 6-digit code to this address:", email, false, true, []string{"Back", "Send code"}, "Send code")
|
||||
if err != nil || btn == "Back" {
|
||||
return "", errCancel
|
||||
}
|
||||
email = strings.TrimSpace(e)
|
||||
if !auth.ValidEmail(email) {
|
||||
say("Please enter a valid email address.")
|
||||
continue
|
||||
}
|
||||
vlog.Printf("mac-key-setup: password reset requested for %s", auth.MaskEmail(email))
|
||||
if ferr := auth.PasswordForgot(email); ferr != nil {
|
||||
vlog.Errorf("mac-key-setup: password forgot %s: %v", auth.MaskEmail(email), ferr)
|
||||
if errors.Is(ferr, auth.ErrResetUnavailable) {
|
||||
offerPortalReset()
|
||||
return "", errCancel
|
||||
}
|
||||
say("Could not send a reset code: " + ferr.Error())
|
||||
continue
|
||||
}
|
||||
msg := "If " + email + " has a Verae account, we emailed it a 6-digit code (valid 15 minutes).\nIt may take a minute; check spam.\n\nCode:"
|
||||
codeLoop:
|
||||
for {
|
||||
btn, code, err := ask(msg, "", false, true, []string{"Cancel", "Send a new code", "Next"}, "Next")
|
||||
if err != nil || btn == "Cancel" {
|
||||
return "", errCancel
|
||||
}
|
||||
if btn == "Send a new code" {
|
||||
if rerr := auth.PasswordForgot(email); rerr != nil {
|
||||
if errors.Is(rerr, auth.ErrResetUnavailable) {
|
||||
offerPortalReset()
|
||||
return "", errCancel
|
||||
}
|
||||
say("Could not send a new code: " + rerr.Error())
|
||||
} else {
|
||||
say("A new code is on its way.")
|
||||
}
|
||||
continue
|
||||
}
|
||||
code = strings.TrimSpace(code)
|
||||
if len(code) != 6 || strings.Trim(code, "0123456789") != "" {
|
||||
say("Enter the 6-digit code from the email.")
|
||||
continue
|
||||
}
|
||||
for {
|
||||
btn, pw, err := ask("Choose a new password.\n"+caps.Policy.Describe(), "", true, true, []string{"Back", "Next"}, "Next")
|
||||
if err != nil || btn == "Back" {
|
||||
continue codeLoop
|
||||
}
|
||||
if perr := caps.Policy.Check(pw, email); perr != nil {
|
||||
say(perr.Error())
|
||||
continue
|
||||
}
|
||||
btn, pw2, err := ask("Confirm the new password:", "", true, true, []string{"Back", "Reset password"}, "Reset password")
|
||||
if err != nil || btn == "Back" {
|
||||
continue
|
||||
}
|
||||
if pw != pw2 {
|
||||
say("The passwords do not match.")
|
||||
continue
|
||||
}
|
||||
pw2 = ""
|
||||
key, rerr := auth.PasswordReset(email, code, pw, tokenName())
|
||||
if rerr != nil {
|
||||
pw = ""
|
||||
vlog.Errorf("mac-key-setup: password reset %s: %v", auth.MaskEmail(email), rerr)
|
||||
if errors.Is(rerr, auth.ErrResetUnavailable) {
|
||||
offerPortalReset()
|
||||
return "", errCancel
|
||||
}
|
||||
say("Could not reset the password: " + rerr.Error())
|
||||
continue codeLoop
|
||||
}
|
||||
vlog.Printf("mac-key-setup: password reset ok for %s (key returned=%v)", auth.MaskEmail(email), key != "")
|
||||
if key != "" {
|
||||
return key, nil
|
||||
}
|
||||
// No key minted (e.g. 2FA account): sign in with the new password.
|
||||
key, lerr := auth.InstallLogin(caps, email, pw, "", tokenName())
|
||||
if errors.Is(lerr, auth.ErrTOTPRequired) {
|
||||
b, totp, cerr := ask("Your password was changed.\n\nThis account uses two-factor authentication. Enter the 6-digit code from your authenticator app:", "", false, true, []string{"Cancel", "Verify"}, "Verify")
|
||||
if cerr != nil || b == "Cancel" {
|
||||
pw = ""
|
||||
return "", errCancel
|
||||
}
|
||||
key, lerr = auth.InstallLogin(caps, email, pw, strings.TrimSpace(totp), tokenName())
|
||||
}
|
||||
pw = ""
|
||||
if lerr != nil {
|
||||
say("Your password was changed, but signing in failed: " + lerr.Error() + "\n\nChoose \"Sign in\" and use your new password.")
|
||||
return "", errCancel
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func macCreate(caps auth.Capabilities) (string, error) {
|
||||
var name, email string
|
||||
for {
|
||||
|
|
|
|||
92
internal/auth/portal_reset.go
Normal file
92
internal/auth/portal_reset.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Password reset by emailed 6-digit code (customer.verae.ai):
|
||||
// POST /portal/api/password/forgot {email} → 202 {ok:true} (always; no enumeration)
|
||||
// POST /portal/api/password/reset {email, code, password, tokenName?}
|
||||
// → 200 {ok:true, token?: "xVerae-…"}
|
||||
// A server without these endpoints (404/401/405/5xx, or 503 email_unavailable)
|
||||
// yields ErrResetUnavailable so the UI can send the user to the web portal.
|
||||
|
||||
// PortalResetURL is where users can reset their password in a browser.
|
||||
const PortalResetURL = "https://customer.verae.ai/portal/"
|
||||
|
||||
var (
|
||||
// ErrResetUnavailable: the server cannot do in-app password reset right now.
|
||||
ErrResetUnavailable = errors.New("password reset isn't available in the app yet")
|
||||
// ErrResetInvalidCode: wrong code (attempts remain).
|
||||
ErrResetInvalidCode = errors.New("that code is not right — check the email and try again")
|
||||
// ErrResetCodeExpired: the code expired; request a new one.
|
||||
ErrResetCodeExpired = errors.New("that code has expired — send a new code")
|
||||
// ErrResetTooManyAttempts: the code was locked after too many tries.
|
||||
ErrResetTooManyAttempts = errors.New("too many wrong codes — send a new code")
|
||||
)
|
||||
|
||||
func resetUnavailableStatus(code int) bool {
|
||||
return code == 404 || code == 401 || code == 405 || code >= 500
|
||||
}
|
||||
|
||||
// PasswordForgot asks the server to email a reset code to email.
|
||||
func PasswordForgot(email string) error {
|
||||
out, code, err := portalPost("/password/forgot", "", map[string]any{"email": strings.TrimSpace(email)})
|
||||
if err != nil {
|
||||
if code == 0 && !strings.HasPrefix(err.Error(), "HTTP") {
|
||||
return err // network / TLS / insecure-base error
|
||||
}
|
||||
if resetUnavailableStatus(code) {
|
||||
return ErrResetUnavailable
|
||||
}
|
||||
if code == 429 {
|
||||
return errors.New("too many requests — wait a few minutes and try again")
|
||||
}
|
||||
if msg, _ := out["error"].(string); msg != "" {
|
||||
return errors.New(msg)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PasswordReset submits the emailed code and a new password. When tokenName is
|
||||
// non-empty the server may mint and return an xVerae- key; key is "" when it
|
||||
// did not (e.g. the account has 2FA) — callers then sign in with the new password.
|
||||
func PasswordReset(email, code, newPassword, tokenName string) (string, error) {
|
||||
body := map[string]any{"email": strings.TrimSpace(email), "code": strings.TrimSpace(code), "password": newPassword}
|
||||
if tokenName != "" {
|
||||
body["tokenName"] = tokenName
|
||||
}
|
||||
out, status, err := portalPost("/password/reset", "", body)
|
||||
if err != nil {
|
||||
if status == 0 && !strings.HasPrefix(err.Error(), "HTTP") {
|
||||
return "", err
|
||||
}
|
||||
if resetUnavailableStatus(status) {
|
||||
return "", ErrResetUnavailable
|
||||
}
|
||||
msg, _ := out["error"].(string)
|
||||
switch msg {
|
||||
case "invalid_code":
|
||||
return "", ErrResetInvalidCode
|
||||
case "code_expired":
|
||||
return "", ErrResetCodeExpired
|
||||
case "too_many_attempts":
|
||||
return "", ErrResetTooManyAttempts
|
||||
case "":
|
||||
return "", err
|
||||
default:
|
||||
if status == 429 {
|
||||
return "", errors.New("too many attempts — wait a few minutes and try again")
|
||||
}
|
||||
return "", errors.New(msg)
|
||||
}
|
||||
}
|
||||
k, _ := out["token"].(string)
|
||||
if strings.HasPrefix(k, "xVerae-") || strings.HasPrefix(k, "key-") {
|
||||
return k, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
114
internal/auth/portal_reset_test.go
Normal file
114
internal/auth/portal_reset_test.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func fakeResetPortal(t *testing.T, forgotStatus int, withToken bool) *[]map[string]any {
|
||||
var calls []map[string]any
|
||||
mux := http.NewServeMux()
|
||||
js := func(w http.ResponseWriter, code int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
mux.HandleFunc("/portal/api/password/forgot", func(w http.ResponseWriter, r *http.Request) {
|
||||
m := map[string]any{}
|
||||
_ = json.NewDecoder(r.Body).Decode(&m)
|
||||
calls = append(calls, m)
|
||||
if forgotStatus == 202 {
|
||||
js(w, 202, map[string]any{"ok": true})
|
||||
return
|
||||
}
|
||||
js(w, forgotStatus, map[string]any{"error": "email_unavailable"})
|
||||
})
|
||||
mux.HandleFunc("/portal/api/password/reset", func(w http.ResponseWriter, r *http.Request) {
|
||||
m := map[string]any{}
|
||||
_ = json.NewDecoder(r.Body).Decode(&m)
|
||||
calls = append(calls, m)
|
||||
switch m["code"] {
|
||||
case "123456":
|
||||
out := map[string]any{"ok": true}
|
||||
if withToken && m["tokenName"] != nil {
|
||||
out["token"] = "xVerae-reset-abcdefgh"
|
||||
}
|
||||
js(w, 200, out)
|
||||
case "999999":
|
||||
js(w, 400, map[string]any{"error": "code_expired"})
|
||||
case "888888":
|
||||
js(w, 400, map[string]any{"error": "too_many_attempts"})
|
||||
default:
|
||||
js(w, 400, map[string]any{"error": "invalid_code"})
|
||||
}
|
||||
})
|
||||
s := httptest.NewServer(mux)
|
||||
t.Cleanup(s.Close)
|
||||
t.Setenv("VERAE_PORTAL_BASE", s.URL)
|
||||
t.Setenv("VERAE_ALLOW_INSECURE_PORTAL", "1")
|
||||
return &calls
|
||||
}
|
||||
|
||||
func TestPasswordForgotAndReset(t *testing.T) {
|
||||
calls := fakeResetPortal(t, 202, true)
|
||||
if err := PasswordForgot(" a@b.co "); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if (*calls)[0]["email"] != "a@b.co" {
|
||||
t.Fatalf("email not trimmed: %v", (*calls)[0])
|
||||
}
|
||||
k, err := PasswordReset("a@b.co", " 123456 ", "new-password-1", "Print Stamp")
|
||||
if err != nil || k != "xVerae-reset-abcdefgh" {
|
||||
t.Fatal(k, err)
|
||||
}
|
||||
if (*calls)[1]["password"] != "new-password-1" || (*calls)[1]["code"] != "123456" {
|
||||
t.Fatalf("reset body %v", (*calls)[1])
|
||||
}
|
||||
if _, err := PasswordReset("a@b.co", "000000", "x", "n"); !errors.Is(err, ErrResetInvalidCode) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := PasswordReset("a@b.co", "999999", "x", "n"); !errors.Is(err, ErrResetCodeExpired) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := PasswordReset("a@b.co", "888888", "x", "n"); !errors.Is(err, ErrResetTooManyAttempts) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordResetNoToken(t *testing.T) {
|
||||
fakeResetPortal(t, 202, false)
|
||||
k, err := PasswordReset("a@b.co", "123456", "new-password-1", "n")
|
||||
if err != nil || k != "" {
|
||||
t.Fatal(k, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordResetUnavailable(t *testing.T) {
|
||||
for _, st := range []int{503, 500} {
|
||||
fakeResetPortal(t, st, true)
|
||||
if err := PasswordForgot("a@b.co"); !errors.Is(err, ErrResetUnavailable) {
|
||||
t.Fatalf("status %d: %v", st, err)
|
||||
}
|
||||
}
|
||||
// Server without the endpoints at all (404) and one requiring auth (401).
|
||||
for _, st := range []int{404, 401} {
|
||||
code := st
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
_, _ = w.Write([]byte(`{"error":"nope"}`))
|
||||
}))
|
||||
t.Setenv("VERAE_PORTAL_BASE", s.URL)
|
||||
t.Setenv("VERAE_ALLOW_INSECURE_PORTAL", "1")
|
||||
if err := PasswordForgot("a@b.co"); !errors.Is(err, ErrResetUnavailable) {
|
||||
t.Fatalf("forgot %d: %v", code, err)
|
||||
}
|
||||
if _, err := PasswordReset("a@b.co", "123456", "pw-12345678", "n"); !errors.Is(err, ErrResetUnavailable) {
|
||||
t.Fatalf("reset %d: %v", code, err)
|
||||
}
|
||||
s.Close()
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ import (
|
|||
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/veraeapi"
|
||||
)
|
||||
|
||||
const HelperVersion = "0.3.3"
|
||||
const HelperVersion = "0.3.4"
|
||||
|
||||
// Result paths written for one print job.
|
||||
type Result struct {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
VERSION="${VERSION:-0.3.3}"
|
||||
VERSION="${VERSION:-0.3.4}"
|
||||
OUT="${OUT:-$ROOT/build/deb}"
|
||||
ARCHS="${1:-all}"
|
||||
PKG_NAME="verae-print"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ SIGNING_ENV="${SIGNING_ENV:-$HOME/fie/installers/macos/signing.env}"
|
|||
# shellcheck source=/dev/null
|
||||
source "$SIGNING_ENV"
|
||||
|
||||
VERSION="${VERSION:-0.3.3}"
|
||||
VERSION="${VERSION:-0.3.4}"
|
||||
BUILD_ROOT="$ROOT/build/macos-pkg"
|
||||
STAGE="$BUILD_ROOT/stage"
|
||||
SCRIPTS="$BUILD_ROOT/scripts"
|
||||
|
|
|
|||
Loading…
Reference in a new issue