Skip to content
Snippets Groups Projects
Commit 7a074b57 authored by meskio's avatar meskio :tent:
Browse files

Add password reset API

parent cb588f75
No related branches found
No related tags found
No related merge requests found
......@@ -21,7 +21,7 @@ func initDB(dbPath string) (*gorm.DB, error) {
}
db.AutoMigrate(&Member{}, &Product{}, &Purchase{}, &Topup{}, &Transaction{},
&OrderPurchase{}, &Order{})
&OrderPurchase{}, &Order{}, &PasswordReset{})
return db, err
}
......@@ -33,12 +33,16 @@ func Init(dbPath string, signKey string, mail *Mail, r *mux.Router) error {
a := api{db, []byte(signKey), mail}
go a.refundOrders()
go a.cleanPaswordResets()
token, err := a.newToken(0, "admin", false)
log.Print(token)
r.HandleFunc("/signin", a.SignIn).Methods("POST")
r.HandleFunc("/token", a.GetToken).Methods("GET")
r.HandleFunc("/reset", a.SendPasswordReset).Methods("POST")
r.HandleFunc("/reset/{token}", a.ValidatePasswordReset).Methods("GET")
r.HandleFunc("/reset/{token}", a.PasswordReset).Methods("PUT")
r.HandleFunc("/member", a.authAdmin(a.ListMembers)).Methods("GET")
r.HandleFunc("/member", a.authAdmin(a.AddMember)).Methods("POST")
......
......@@ -3,21 +3,45 @@ package api
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"encoding/json"
"errors"
"log"
"net/http"
"net/url"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/gorilla/mux"
"golang.org/x/crypto/argon2"
"gorm.io/gorm"
)
const (
timeExpireResetToken = 2 * 24 * time.Hour
)
type PasswordReset struct {
gorm.Model
Token string `gorm:"unique;index"`
MemberNum int `gorm:"column:member"`
Member *Member `gorm:"foreignKey:MemberNum;references:Num"`
}
type creds struct {
Login string `json:"login"`
Password string `json:"password"`
NoExpire bool `json:"noExpire"`
}
type passwordResetPost struct {
Email string `json:"email"`
}
type passwordResetPut struct {
Password string `json:"password"`
}
func (a *api) SignIn(w http.ResponseWriter, req *http.Request) {
var c creds
err := json.NewDecoder(req.Body).Decode(&c)
......@@ -95,6 +119,152 @@ func (a *api) GetToken(w http.ResponseWriter, req *http.Request) {
}
}
func (a *api) SendPasswordReset(w http.ResponseWriter, req *http.Request) {
var reset passwordResetPost
err := json.NewDecoder(req.Body).Decode(&reset)
if err != nil {
log.Printf("Can't decode password reset request: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
var member Member
err = a.db.Where("email = ?", reset.Email).First(&member).Error
if err != nil {
log.Printf("Can't locate user %s: %v", reset.Email, err)
w.WriteHeader(http.StatusBadRequest)
return
}
tokenBytes := make([]byte, 15)
_, err = rand.Read(tokenBytes)
if err != nil {
log.Printf("Can't generate a random token for password reset: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
token := base64.URLEncoding.EncodeToString(tokenBytes)
passwordReset := PasswordReset{
Token: token,
MemberNum: member.Num,
}
url := url.URL{
Scheme: req.URL.Scheme,
Host: req.URL.Host,
Path: "/api/reset/" + token,
}
err = a.db.Transaction(func(tx *gorm.DB) error {
err = tx.Create(&passwordReset).Error
if err != nil {
return err
}
return a.mail.sendPasswordReset(member, url.String())
})
if err != nil {
log.Printf("Error sending password reset: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write([]byte("Email sent"))
w.WriteHeader(http.StatusCreated)
}
func (a *api) ValidatePasswordReset(w http.ResponseWriter, req *http.Request) {
passwordReset, status := a.getPasswordReset(req)
if status != http.StatusOK {
w.WriteHeader(status)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(passwordReset.Member)
if err != nil {
log.Printf("Can't encode reset member: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (a *api) PasswordReset(w http.ResponseWriter, req *http.Request) {
passwordReset, status := a.getPasswordReset(req)
if status != http.StatusOK {
w.WriteHeader(status)
return
}
var reset passwordResetPut
err := json.NewDecoder(req.Body).Decode(&reset)
if err != nil {
log.Printf("Can't decode password reset put: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
hash, salt, err := newHashPass(reset.Password)
if err != nil {
log.Printf("Can't hash password: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
err = a.db.Transaction(func(tx *gorm.DB) error {
err := a.db.Model(&Member{}).
Updates(Member{
PassHash: hash,
Salt: salt,
}).Error
if err != nil {
return err
}
return a.db.Delete(passwordReset).Error
})
if err != nil {
log.Printf("Error updating password: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write([]byte("Email sent"))
w.WriteHeader(http.StatusAccepted)
}
func (a *api) getPasswordReset(req *http.Request) (PasswordReset, int) {
vars := mux.Vars(req)
token := vars["token"]
var passwordReset PasswordReset
err := a.db.Where("token = ?", token).
Preload("Member").
First(&passwordReset).Error
status := http.StatusOK
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
status = http.StatusNotFound
} else {
log.Printf("Can't get password reset %s: %v", token, err)
status = http.StatusInternalServerError
}
}
return passwordReset, status
}
func (a *api) cleanPaswordResets() {
time.Sleep(time.Minute)
const refundSleeptime = 10 * time.Minute
for {
time.Sleep(refundSleeptime)
a.cleanReset()
}
}
func (a *api) cleanReset() {
t := time.Now().Add(timeExpireResetToken)
res := a.db.Where("created_at < ?", true, t).
Delete(&PasswordReset{})
if res.Error != nil {
log.Println("Error deleting old reset tokens:", res.Error)
} else if res.RowsAffected != 0 {
log.Println("Deleted", res.RowsAffected, "password reset tokens")
}
}
func (a *api) auth(fn func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
token := req.Header.Get("x-authentication")
......@@ -223,7 +393,7 @@ func (a *api) validateToken(token string) (bool, jwt.MapClaims) {
}
func newHashPass(password string) (hash []byte, salt []byte, err error) {
salt = make([]byte, 32)
salt = make([]byte, 16)
_, err = rand.Read(salt)
if err != nil {
return
......
......@@ -11,9 +11,11 @@ const (
orderTmpl = `To: {{.To}}
From: {{.From}}
Content-Type: text/plain; charset="utf-8"
Subject: [garbanzo] pedido {{.Name}}
Subject: [garbanzo] pedido {{.OrderName}}
El pedido {{.Name}} a sido cerrado.
Hola {{.MemberName}},
El pedido {{.OrderName}} a sido cerrado.
Se han pedido:{{range $name, $amount := .Products}}
* {{$name}}: {{$amount}}{{end}}
......@@ -23,6 +25,24 @@ Las siguientes personas han pedido:
{{$name}}:{{range $purchases}}
* {{.Product.Name}}: {{.Amount}}{{end}}
{{end}}
Salud y garbancicos.
`
resetTmpl = `To: {{.To}}
From: {{.From}}
Content-Type: text/plain; charset="utf-8"
Subject: [garbanzo] recupera tu contraseña
Hola {{.Name}},
Hemos recivido una petición para recuperar tu contraseña en el Garbanzo Negro.
Si no has pedido cambiar tu contraseña ignora este email o si siguen llegandote
emails como este informa a las administradoras.
Para cambiar tu contraseña visita el siguiente enlace y sigue las instrucciones:
{{.Link}}
Salud y garbancicos.
`
)
......@@ -46,11 +66,12 @@ func NewMail(email, password, server string) *Mail {
}
type orderData struct {
To string
From string
Name string
Products map[string]int
Purchases map[string][]OrderPurchase
To string
From string
MemberName string
OrderName string
Products map[string]int
Purchases map[string][]OrderPurchase
}
func (m Mail) sendOrder(to string, order *Order) error {
......@@ -72,11 +93,12 @@ func (m Mail) sendOrder(to string, order *Order) error {
purchases[t.Member.Name] = purchase
}
data := orderData{
To: to,
From: m.email,
Name: order.Name,
Products: products,
Purchases: purchases,
To: to,
From: m.email,
OrderName: order.Name,
MemberName: order.Member.Name,
Products: products,
Purchases: purchases,
}
var buff bytes.Buffer
......@@ -86,3 +108,30 @@ func (m Mail) sendOrder(to string, order *Order) error {
}
return smtp.SendMail(m.server, m.auth, m.email, []string{to}, buff.Bytes())
}
type passwordResetData struct {
To string
From string
Name string
Link string
}
func (m Mail) sendPasswordReset(member Member, link string) error {
if m.server == "" {
return nil
}
var buff bytes.Buffer
data := passwordResetData{
To: member.Email,
From: m.email,
Name: member.Name,
Link: link,
}
err := m.tmpl.ExecuteTemplate(&buff, "order", data)
if err != nil {
return err
}
return smtp.SendMail(m.server, m.auth, m.email, []string{member.Email}, buff.Bytes())
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment