api,frontend: add commenter password resets
This commit is contained in:
@@ -46,3 +46,4 @@ var errorDatabaseMigration = errors.New("Encountered error applying database mig
|
||||
var errorNoSuchUnsubscribeSecretHex = errors.New("Invalid unsubscribe link.")
|
||||
var errorEmptyPaths = errors.New("Empty paths field.")
|
||||
var errorInvalidDomain = errors.New("Invalid domain name. Do not include the URL path after the forward slash.")
|
||||
var errorInvalidEntity = errors.New("That entity does not exist.")
|
||||
|
97
api/forgot.go
Normal file
97
api/forgot.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func forgot(email string, entity string) error {
|
||||
if email == "" {
|
||||
return errorMissingField
|
||||
}
|
||||
|
||||
if entity != "owner" && entity != "commenter" {
|
||||
return errorInvalidEntity
|
||||
}
|
||||
|
||||
if !smtpConfigured {
|
||||
return errorSmtpNotConfigured
|
||||
}
|
||||
|
||||
var hex string
|
||||
var name string
|
||||
if entity == "owner" {
|
||||
o, err := ownerGetByEmail(email)
|
||||
if err != nil {
|
||||
if err == errorNoSuchEmail {
|
||||
// TODO: use a more random time instead.
|
||||
time.Sleep(1 * time.Second)
|
||||
return nil
|
||||
} else {
|
||||
logger.Errorf("cannot get owner by email: %v", err)
|
||||
return errorInternal
|
||||
}
|
||||
}
|
||||
hex = o.OwnerHex
|
||||
name = o.Name
|
||||
} else {
|
||||
c, err := commenterGetByEmail("commento", email)
|
||||
if err != nil {
|
||||
if err == errorNoSuchEmail {
|
||||
// TODO: use a more random time instead.
|
||||
time.Sleep(1 * time.Second)
|
||||
return nil
|
||||
} else {
|
||||
logger.Errorf("cannot get commenter by email: %v", err)
|
||||
return errorInternal
|
||||
}
|
||||
}
|
||||
hex = c.CommenterHex
|
||||
name = c.Name
|
||||
}
|
||||
|
||||
resetHex, err := randomHex(32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var statement string
|
||||
|
||||
statement = `
|
||||
INSERT INTO
|
||||
resetHexes (resetHex, hex, entity, sendDate)
|
||||
VALUES ($1, $2, $3, $4 );
|
||||
`
|
||||
_, err = db.Exec(statement, resetHex, hex, entity, time.Now().UTC())
|
||||
if err != nil {
|
||||
logger.Errorf("cannot insert resetHex: %v", err)
|
||||
return errorInternal
|
||||
}
|
||||
|
||||
err = smtpResetHex(email, name, resetHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func forgotHandler(w http.ResponseWriter, r *http.Request) {
|
||||
type request struct {
|
||||
Email *string `json:"email"`
|
||||
Entity *string `json:"entity"`
|
||||
}
|
||||
|
||||
var x request
|
||||
if err := bodyUnmarshal(r, &x); err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := forgot(*x.Email, *x.Entity); err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
bodyMarshal(w, response{"success": true})
|
||||
}
|
@@ -1,70 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ownerSendResetHex(email string) error {
|
||||
if email == "" {
|
||||
return errorMissingField
|
||||
}
|
||||
|
||||
if !smtpConfigured {
|
||||
return errorSmtpNotConfigured
|
||||
}
|
||||
|
||||
o, err := ownerGetByEmail(email)
|
||||
if err != nil {
|
||||
if err == errorNoSuchEmail {
|
||||
// TODO: use a more random time instead.
|
||||
time.Sleep(1 * time.Second)
|
||||
return nil
|
||||
} else {
|
||||
logger.Errorf("cannot get owner by email: %v", err)
|
||||
return errorInternal
|
||||
}
|
||||
}
|
||||
|
||||
resetHex, err := randomHex(32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
statement := `
|
||||
INSERT INTO
|
||||
ownerResetHexes (resetHex, ownerHex, sendDate)
|
||||
VALUES ($1, $2, $3 );
|
||||
`
|
||||
_, err = db.Exec(statement, resetHex, o.OwnerHex, time.Now().UTC())
|
||||
if err != nil {
|
||||
logger.Errorf("cannot insert resetHex: %v", err)
|
||||
return errorInternal
|
||||
}
|
||||
|
||||
err = smtpOwnerResetHex(email, o.Name, resetHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ownerSendResetHexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
type request struct {
|
||||
Email *string `json:"email"`
|
||||
}
|
||||
|
||||
var x request
|
||||
if err := bodyUnmarshal(r, &x); err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := ownerSendResetHex(*x.Email); err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
bodyMarshal(w, response{"success": true})
|
||||
}
|
@@ -1,73 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func ownerResetPassword(resetHex string, password string) error {
|
||||
if resetHex == "" || password == "" {
|
||||
return errorMissingField
|
||||
}
|
||||
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot generate hash from password: %v\n", err)
|
||||
return errorInternal
|
||||
}
|
||||
|
||||
statement := `
|
||||
UPDATE owners SET passwordHash=$1
|
||||
WHERE ownerHex = (
|
||||
SELECT ownerHex
|
||||
FROM ownerResetHexes
|
||||
WHERE resetHex=$2
|
||||
);
|
||||
`
|
||||
res, err := db.Exec(statement, string(passwordHash), resetHex)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot change user's password: %v\n", err)
|
||||
return errorInternal
|
||||
}
|
||||
|
||||
count, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
logger.Errorf("cannot count rows affected: %v\n", err)
|
||||
return errorInternal
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return errorNoSuchResetToken
|
||||
}
|
||||
|
||||
statement = `
|
||||
DELETE FROM ownerResetHexes
|
||||
WHERE resetHex=$1;
|
||||
`
|
||||
_, err = db.Exec(statement, resetHex)
|
||||
if err != nil {
|
||||
logger.Warningf("cannot remove reset token: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ownerResetPasswordHandler(w http.ResponseWriter, r *http.Request) {
|
||||
type request struct {
|
||||
ResetHex *string `json:"resetHex"`
|
||||
Password *string `json:"password"`
|
||||
}
|
||||
|
||||
var x request
|
||||
if err := bodyUnmarshal(r, &x); err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := ownerResetPassword(*x.ResetHex, *x.Password); err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
bodyMarshal(w, response{"success": true})
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestOwnerResetPasswordBasics(t *testing.T) {
|
||||
failTestOnError(t, setupTestEnv())
|
||||
|
||||
ownerHex, _ := ownerNew("test@example.com", "Test", "hunter2")
|
||||
|
||||
resetHex, _ := randomHex(32)
|
||||
|
||||
statement := `
|
||||
INSERT INTO
|
||||
ownerResetHexes (resetHex, ownerHex, sendDate)
|
||||
VALUES ($1, $2, $3 );
|
||||
`
|
||||
_, err := db.Exec(statement, resetHex, ownerHex, time.Now().UTC())
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error inserting resetHex: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ownerResetPassword(resetHex, "hunter3"); err != nil {
|
||||
t.Errorf("unexpected error resetting password: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := ownerLogin("test@example.com", "hunter2"); err == nil {
|
||||
t.Errorf("expected error not found when given old password")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := ownerLogin("test@example.com", "hunter3"); err != nil {
|
||||
t.Errorf("unexpected error when logging in: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
82
api/reset.go
Normal file
82
api/reset.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func reset(resetHex string, password string) (string, error) {
|
||||
if resetHex == "" || password == "" {
|
||||
return "", errorMissingField
|
||||
}
|
||||
|
||||
statement := `
|
||||
SELECT hex, entity
|
||||
FROM resetHexes
|
||||
WHERE resetHex = $1;
|
||||
`
|
||||
row := db.QueryRow(statement, resetHex)
|
||||
|
||||
var hex string
|
||||
var entity string
|
||||
if err := row.Scan(&hex, &entity); err != nil {
|
||||
// TODO: is this the only error?
|
||||
return "", errorNoSuchResetToken
|
||||
}
|
||||
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot generate hash from password: %v\n", err)
|
||||
return "", errorInternal
|
||||
}
|
||||
|
||||
if entity == "owner" {
|
||||
statement = `
|
||||
UPDATE owners SET passwordHash = $1
|
||||
WHERE ownerHex = $2;
|
||||
`
|
||||
} else {
|
||||
statement = `
|
||||
UPDATE commenters SET passwordHash = $1
|
||||
WHERE commenterHex = $2;
|
||||
`
|
||||
}
|
||||
|
||||
_, err = db.Exec(statement, string(passwordHash), hex)
|
||||
if err != nil {
|
||||
logger.Errorf("cannot change %s's password: %v\n", entity, err)
|
||||
return "", errorInternal
|
||||
}
|
||||
|
||||
statement = `
|
||||
DELETE FROM resetHexes
|
||||
WHERE resetHex = $1;
|
||||
`
|
||||
_, err = db.Exec(statement, resetHex)
|
||||
if err != nil {
|
||||
logger.Warningf("cannot remove resetHex: %v\n", err)
|
||||
}
|
||||
|
||||
return entity, nil
|
||||
}
|
||||
|
||||
func resetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
type request struct {
|
||||
ResetHex *string `json:"resetHex"`
|
||||
Password *string `json:"password"`
|
||||
}
|
||||
|
||||
var x request
|
||||
if err := bodyUnmarshal(r, &x); err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
entity, err := reset(*x.ResetHex, *x.Password)
|
||||
if err != nil {
|
||||
bodyMarshal(w, response{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
bodyMarshal(w, response{"success": true, "entity": entity})
|
||||
}
|
@@ -8,8 +8,6 @@ func apiRouterInit(router *mux.Router) error {
|
||||
router.HandleFunc("/api/owner/new", ownerNewHandler).Methods("POST")
|
||||
router.HandleFunc("/api/owner/confirm-hex", ownerConfirmHexHandler).Methods("GET")
|
||||
router.HandleFunc("/api/owner/login", ownerLoginHandler).Methods("POST")
|
||||
router.HandleFunc("/api/owner/send-reset-hex", ownerSendResetHexHandler).Methods("POST")
|
||||
router.HandleFunc("/api/owner/reset-password", ownerResetPasswordHandler).Methods("POST")
|
||||
router.HandleFunc("/api/owner/self", ownerSelfHandler).Methods("POST")
|
||||
|
||||
router.HandleFunc("/api/domain/new", domainNewHandler).Methods("POST")
|
||||
@@ -31,6 +29,9 @@ func apiRouterInit(router *mux.Router) error {
|
||||
router.HandleFunc("/api/commenter/self", commenterSelfHandler).Methods("POST")
|
||||
router.HandleFunc("/api/commenter/photo", commenterPhotoHandler).Methods("GET")
|
||||
|
||||
router.HandleFunc("/api/forgot", forgotHandler).Methods("POST")
|
||||
router.HandleFunc("/api/reset", resetHandler).Methods("POST")
|
||||
|
||||
router.HandleFunc("/api/email/get", emailGetHandler).Methods("POST")
|
||||
router.HandleFunc("/api/email/update", emailUpdateHandler).Methods("POST")
|
||||
router.HandleFunc("/api/email/moderate", emailModerateHandler).Methods("GET")
|
||||
|
@@ -96,7 +96,7 @@ func staticRouterInit(router *mux.Router) error {
|
||||
pages := []string{
|
||||
"/login",
|
||||
"/forgot",
|
||||
"/reset-password",
|
||||
"/reset",
|
||||
"/signup",
|
||||
"/confirm-email",
|
||||
"/unsubscribe",
|
||||
|
@@ -6,17 +6,17 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type ownerResetHexPlugs struct {
|
||||
type resetHexPlugs struct {
|
||||
Origin string
|
||||
ResetHex string
|
||||
}
|
||||
|
||||
func smtpOwnerResetHex(to string, toName string, resetHex string) error {
|
||||
func smtpResetHex(to string, toName string, resetHex string) error {
|
||||
var header bytes.Buffer
|
||||
headerTemplate.Execute(&header, &headerPlugs{FromAddress: os.Getenv("SMTP_FROM_ADDRESS"), ToAddress: to, ToName: toName, Subject: "Reset your password"})
|
||||
|
||||
var body bytes.Buffer
|
||||
templates["reset-hex"].Execute(&body, &ownerResetHexPlugs{Origin: os.Getenv("ORIGIN"), ResetHex: resetHex})
|
||||
templates["reset-hex"].Execute(&body, &resetHexPlugs{Origin: os.Getenv("ORIGIN"), ResetHex: resetHex})
|
||||
|
||||
err := smtp.SendMail(os.Getenv("SMTP_HOST")+":"+os.Getenv("SMTP_PORT"), smtpAuth, os.Getenv("SMTP_FROM_ADDRESS"), []string{to}, concat(header, body))
|
||||
if err != nil {
|
Reference in New Issue
Block a user