From 3e6f43b0d358da37fb56db7a4d8026344505b8f6 Mon Sep 17 00:00:00 2001
From: meskio <meskio@sindominio.net>
Date: Tue, 13 Oct 2020 13:09:09 +0200
Subject: [PATCH] Use login name and allow changing password

---
 api/api.go         |   1 +
 api/auth.go        |  10 ++---
 api/auth_test.go   |   4 +-
 api/member.go      | 100 +++++++++++++++++++++++++++++++++++----------
 api/member_test.go |  49 +++++++++++++++++-----
 setup.sh           |   4 +-
 src/SignIn.js      |   4 +-
 7 files changed, 128 insertions(+), 44 deletions(-)

diff --git a/api/api.go b/api/api.go
index e072e36..a9f5069 100644
--- a/api/api.go
+++ b/api/api.go
@@ -42,6 +42,7 @@ func Init(dbPath string, signKey string, r *mux.Router) error {
 	r.HandleFunc("/member", a.authAdmin(a.ListMembers)).Methods("GET")
 	r.HandleFunc("/member", a.authAdmin(a.AddMember)).Methods("POST")
 	r.HandleFunc("/member/me", a.authNum(a.getMemberNum)).Methods("GET")
+	r.HandleFunc("/member/me", a.authNum(a.UpdateMemberMe)).Methods("PUT")
 	r.HandleFunc("/member/{num:[0-9]+}", a.authAdmin(a.GetMember)).Methods("GET")
 	r.HandleFunc("/member/{num:[0-9]+}", a.authAdmin(a.UpdateMember)).Methods("PUT")
 	r.HandleFunc("/member/{num:[0-9]+}", a.authAdmin(a.DeleteMember)).Methods("DELETE")
diff --git a/api/auth.go b/api/auth.go
index a31bea5..b6881e7 100644
--- a/api/auth.go
+++ b/api/auth.go
@@ -13,7 +13,7 @@ import (
 )
 
 type creds struct {
-	Name     string `json:"name"`
+	Login    string `json:"login"`
 	Password string `json:"password"`
 	NoExpire bool   `json:"noExpire"`
 }
@@ -27,21 +27,21 @@ func (a *api) SignIn(w http.ResponseWriter, req *http.Request) {
 		return
 	}
 	var member Member
-	err = a.db.Where("name = ?", c.Name).First(&member).Error
+	err = a.db.Where("login = ?", c.Login).First(&member).Error
 	if err != nil {
-		log.Printf("Can't locate user %s: %v", c.Name, err)
+		log.Printf("Can't locate user %s: %v", c.Login, err)
 		w.WriteHeader(http.StatusBadRequest)
 		return
 	}
 
 	hash := hashPass(c.Password, member.Salt)
 	if subtle.ConstantTimeCompare(hash, member.PassHash) == 0 {
-		log.Printf("Invalid pass for %s", c.Name)
+		log.Printf("Invalid pass for %s", c.Login)
 		w.WriteHeader(http.StatusBadRequest)
 		return
 	}
 
-	log.Printf("Logged in as %s", c.Name)
+	log.Printf("Logged in as %s", c.Login)
 	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(http.StatusOK)
 
diff --git a/api/auth_test.go b/api/auth_test.go
index 6f70a08..5c49308 100644
--- a/api/auth_test.go
+++ b/api/auth_test.go
@@ -21,14 +21,14 @@ func TestSignIn(t *testing.T) {
 		Member Member `json:"member"`
 	}
 	jsonAuth := creds{
-		Name:     testMemberAdmin.Name,
+		Login:    testMemberAdmin.Login,
 		Password: testMemberAdmin.Password,
 	}
 	resp = tapi.do("POST", "/signin", jsonAuth, &respMember)
 	if resp.StatusCode != http.StatusOK {
 		t.Fatal("Can't sign in:", resp.Status)
 	}
-	if respMember.Member.Name != testMemberAdmin.Name {
+	if respMember.Member.Login != testMemberAdmin.Login {
 		t.Fatal("Unexpected member:", respMember)
 	}
 	tapi.token = respMember.Token
diff --git a/api/member.go b/api/member.go
index 353e92d..2a72b31 100644
--- a/api/member.go
+++ b/api/member.go
@@ -14,7 +14,8 @@ import (
 type Member struct {
 	gorm.Model `json:"-"`
 	Num        int    `json:"num" gorm:"unique;index"`
-	Name       string `json:"name" gorm:"unique;index"`
+	Login      string `json:"login" gorm:"unique;index"`
+	Name       string `json:"name"`
 	Email      string `json:"email"`
 	Phone      string `json:"phone"`
 	Balance    int    `json:"balance"`
@@ -23,11 +24,13 @@ type Member struct {
 	Salt       []byte `json:"-"`
 }
 
+type MemberReq struct {
+	Member
+	Password string `json:"password"`
+}
+
 func (a *api) AddMember(w http.ResponseWriter, req *http.Request) {
-	var memberReq struct {
-		Member
-		Password string `json:"password"`
-	}
+	var memberReq MemberReq
 	err := json.NewDecoder(req.Body).Decode(&memberReq)
 	if err != nil {
 		log.Printf("Can't create member: %v", err)
@@ -36,8 +39,10 @@ func (a *api) AddMember(w http.ResponseWriter, req *http.Request) {
 	}
 	member := Member{
 		Num:     memberReq.Num,
+		Login:   memberReq.Login,
 		Name:    memberReq.Name,
 		Email:   memberReq.Email,
+		Phone:   memberReq.Phone,
 		Balance: memberReq.Balance,
 		Role:    memberReq.Role,
 	}
@@ -122,7 +127,7 @@ func (a *api) DeleteMember(w http.ResponseWriter, req *http.Request) {
 }
 
 func (a *api) UpdateMember(w http.ResponseWriter, req *http.Request) {
-	var member Member
+	var member MemberReq
 	err := json.NewDecoder(req.Body).Decode(&member)
 	if err != nil {
 		log.Printf("Can't decode memeber: %v", err)
@@ -131,43 +136,94 @@ func (a *api) UpdateMember(w http.ResponseWriter, req *http.Request) {
 	}
 
 	vars := mux.Vars(req)
-	var dbMember Member
-	err = a.db.Where("num = ?", vars["num"]).First(&dbMember).Error
+	num, err := strconv.Atoi(vars["num"])
+	if err != nil {
+		log.Printf("Invalid member num %s: %v", vars["num"], err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	m, err := a.updateMember(num, member)
 	if err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
 			w.WriteHeader(http.StatusNotFound)
 			return
 		}
-		log.Printf("Can't get member %s: %v", vars["num"], err)
+		log.Printf("Can't update member %d: %v", num, err)
 		w.WriteHeader(http.StatusInternalServerError)
 		return
 	}
 
-	if member.Num != 0 {
-		dbMember.Num = member.Num
-	}
-	if member.Name != "" {
-		dbMember.Name = member.Name
-	}
-	if member.Email != "" {
-		dbMember.Email = member.Email
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusAccepted)
+	err = json.NewEncoder(w).Encode(m)
+	if err != nil {
+		log.Printf("Can't encode updated memeber: %v", err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
 	}
-	if member.Balance >= 0 {
-		dbMember.Balance = member.Balance
+}
+
+func (a *api) UpdateMemberMe(num int, w http.ResponseWriter, req *http.Request) {
+	var member MemberReq
+	err := json.NewDecoder(req.Body).Decode(&member)
+	if err != nil {
+		log.Printf("Can't decode memeber: %v", err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
 	}
-	err = a.db.Save(&dbMember).Error
+
+	member.Num = 0
+	member.Balance = -1
+	m, err := a.updateMember(num, member)
 	if err != nil {
-		log.Printf("Can't update member %s: %v %v", vars["num"], err, member)
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			w.WriteHeader(http.StatusNotFound)
+			return
+		}
+		log.Printf("Can't update member %d: %v", num, err)
 		w.WriteHeader(http.StatusInternalServerError)
 		return
 	}
 
 	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(http.StatusAccepted)
-	err = json.NewEncoder(w).Encode(dbMember)
+	err = json.NewEncoder(w).Encode(m)
 	if err != nil {
 		log.Printf("Can't encode updated memeber: %v", err)
 		w.WriteHeader(http.StatusInternalServerError)
 		return
 	}
 }
+
+func (a *api) updateMember(num int, member MemberReq) (Member, error) {
+	var dbMember Member
+	err := a.db.Where("num = ?", num).First(&dbMember).Error
+	if err != nil {
+		return dbMember, err
+	}
+
+	if member.Num != 0 {
+		dbMember.Num = member.Num
+	}
+	if member.Login != "" {
+		dbMember.Login = member.Login
+	}
+	if member.Name != "" {
+		dbMember.Name = member.Name
+	}
+	if member.Email != "" {
+		dbMember.Email = member.Email
+	}
+	if member.Balance >= 0 {
+		dbMember.Balance = member.Balance
+	}
+	if member.Password != "" {
+		dbMember.PassHash, dbMember.Salt, err = newHashPass(member.Password)
+		if err != nil {
+			return dbMember, err
+		}
+	}
+	err = a.db.Save(&dbMember).Error
+	return dbMember, err
+}
diff --git a/api/member_test.go b/api/member_test.go
index 2712ab4..7758fa4 100644
--- a/api/member_test.go
+++ b/api/member_test.go
@@ -5,13 +5,11 @@ import (
 	"testing"
 )
 
-var testMember = struct {
-	Member
-	Password string `json:"password"`
-}{
+var testMember = MemberReq{
 	Member: Member{
 		Num:     10,
-		Name:    "foo",
+		Login:   "foo",
+		Name:    "Foo Baz",
 		Email:   "foo@example.com",
 		Role:    "",
 		Balance: 10000,
@@ -19,13 +17,11 @@ var testMember = struct {
 	Password: "password",
 }
 
-var testMemberAdmin = struct {
-	Member
-	Password string `json:"password"`
-}{
+var testMemberAdmin = MemberReq{
 	Member: Member{
 		Num:     20,
-		Name:    "bar",
+		Login:   "bar",
+		Name:    "Bar Baz",
 		Email:   "bar@example.com",
 		Role:    "admin",
 		Balance: 15000,
@@ -47,7 +43,7 @@ func TestMemberAddList(t *testing.T) {
 	if len(members) != 2 {
 		t.Fatal("Wrong number of members", len(members), members)
 	}
-	if members[0].Name != "foo" {
+	if members[0].Name != testMember.Name {
 		t.Error("Wrong name:", members[0].Name)
 	}
 	if members[0].Email != "foo@example.com" {
@@ -106,6 +102,37 @@ func TestMemberUpdate(t *testing.T) {
 	}
 }
 
+func TestMemberUpdateMe(t *testing.T) {
+	tapi := newTestAPI(t)
+	defer tapi.close()
+
+	tapi.addTestMember()
+	member := testMember
+	member.Password = "foobar"
+	member.Email = "other@example.com"
+	resp := tapi.do("PUT", "/member/me", member, nil)
+	if resp.StatusCode != http.StatusAccepted {
+		t.Fatal("Can't update member:", resp.Status)
+	}
+
+	var gotMember Member
+	resp = tapi.doAdmin("GET", "/member/10", nil, &gotMember)
+	if resp.StatusCode != http.StatusOK {
+		t.Error("Can't find the member:", resp.Status)
+	}
+	if gotMember.Email != member.Email {
+		t.Error("Wrong email:", gotMember)
+	}
+	jsonAuth := creds{
+		Login:    testMember.Login,
+		Password: member.Password,
+	}
+	resp = tapi.do("POST", "/signin", jsonAuth, nil)
+	if resp.StatusCode != http.StatusOK {
+		t.Fatal("Can't sign in:", resp.Status)
+	}
+}
+
 func (tapi *testAPI) addTestMember() {
 	resp := tapi.doAdmin("POST", "/member", testMember, nil)
 	if resp.StatusCode != http.StatusCreated {
diff --git a/setup.sh b/setup.sh
index abace7d..5e3d79b 100755
--- a/setup.sh
+++ b/setup.sh
@@ -4,8 +4,8 @@ TOKEN="$1"
 ADDR="localhost:8080"
 
 # set up members
-curl -H "x-authentication: $TOKEN" -X "POST" -d '{"num": 900, "name": "admin", "email": "admin@example.com", "phone": "654321123", "password": "admin", "role": "admin", "balance": 10000}' $ADDR/api/member
-curl -H "x-authentication: $TOKEN" -X "POST" -d '{"num": 901, "name": "socia", "email": "socia@example.com", "phone": "678900123", "password": "socia", "balance": 10000}' $ADDR/api/member
+curl -H "x-authentication: $TOKEN" -X "POST" -d '{"num": 900, "login": "admin", "name": "Administradora", "email": "admin@example.com", "phone": "654321123", "password": "admin", "role": "admin", "balance": 10000}' $ADDR/api/member
+curl -H "x-authentication: $TOKEN" -X "POST" -d '{"num": 901, "login": "socia", "name": "Socia Aicos", "email": "socia@example.com", "phone": "678900123", "password": "socia", "balance": 10000}' $ADDR/api/member
 
 # set up products
 curl -H "x-authentication: $TOKEN" -X "POST" -d '{"code": 234, "name": "aceite", "price": 1700, "stock": 35}' $ADDR/api/product
diff --git a/src/SignIn.js b/src/SignIn.js
index 41157b6..8f2dbbb 100644
--- a/src/SignIn.js
+++ b/src/SignIn.js
@@ -22,7 +22,7 @@ class SignIn extends React.Component {
 
     this.setState({ isLoading: true, error: null });
     const body = JSON.stringify({
-      name: this.state.name,
+      login: this.state.name,
       password: this.state.password,
       noExpire: this.state.noExpire,
     });
@@ -57,7 +57,7 @@ class SignIn extends React.Component {
     let form = (
       <Form onSubmit={this.onFormSubmit}>
         <Form.Group>
-          <Form.Label>Nombre</Form.Label>
+          <Form.Label>Nombre de acceso</Form.Label>
           <Form.Control
             placeholder="Nombre"
             value={this.state.name}
-- 
GitLab