From 2efbffd22303eb9065e0b4a41514c9168f6fc8c4 Mon Sep 17 00:00:00 2001 From: Tobias Wackenhut Date: Thu, 27 Feb 2020 22:44:34 +0100 Subject: [PATCH 1/2] add AddCard functionality - Add channel to Kasse struct to hand over UIDs to registration - Add HTTP handlers for adding cards --- http.go | 152 ++++++++++++++++++++++++++++++++++++++- main.go | 54 +++++++++++--- main_test.go | 50 +++++++------ templates/add_card.html | 70 ++++++++++++++++++ templates/dashboard.html | 6 +- templates/layout.html | 30 ++++---- templates/login.html | 10 +-- 7 files changed, 319 insertions(+), 53 deletions(-) create mode 100644 templates/add_card.html diff --git a/http.go b/http.go index 2836ba4..8a62a5f 100644 --- a/http.go +++ b/http.go @@ -2,9 +2,11 @@ package main import ( "bytes" - "net/http" - + "context" + "encoding/hex" "github.com/gorilla/mux" + "net/http" + "time" ) // GetLoginPage renders the login page to the user. @@ -203,6 +205,149 @@ func (k *Kasse) GetLogout(res http.ResponseWriter, req *http.Request) { } } +// GetAddCard renders the add card dialog. The rendered template contains an instruction for the browser to connect to AddCardEvent and listen for the card UID on the next swipe +func (k *Kasse) GetAddCard(res http.ResponseWriter, req *http.Request) { + session, err := k.sessions.Get(req, "nnev-kasse") + if err != nil { + http.Redirect(res, req, "/login.html", 302) + return + } + ui, ok := session.Values["user"] + if !ok { + http.Redirect(res, req, "/login.html", 302) + return + } + + user := ui.(User) + data := struct { + User *User + Description string + Message string + }{ + User: &user, + Description: "", + Message: "", + } + + if err := ExecuteTemplate(res, TemplateInput{Title: "ccchd Kasse", Body: "add_card.html", Data: &data}); err != nil { + k.log.Println("Could not render template:", err) + http.Error(res, "Internal error", 500) + return + } +} + +// AddCardEvent returns a json containing the next swiped card UID. The UID is obtained using a channel which is written by the HandleCard method +func (k *Kasse) AddCardEvent(res http.ResponseWriter, req *http.Request) { + session, err := k.sessions.Get(req, "nnev-kasse") + if err != nil { + http.Redirect(res, req, "/login.html", 302) + return + } + _, ok := session.Values["user"] + if !ok { + http.Redirect(res, req, "/login.html", 302) + return + } + + res.Header().Set("Content-Type", "text/event-stream; charset=utf-8") + res.WriteHeader(http.StatusOK) + + // Only one go routine can listen on the next card swipe. Tell the client, when it obtains the lock + k.registration.Lock() + defer k.registration.Unlock() + if _, err := res.Write([]byte("event: lock\ndata: lock\n\n")); err != nil { + k.log.Println("Could not write: ", err) + } + if f, ok := res.(http.Flusher); ok { + f.Flush() + } + + k.log.Println("Waiting for Card") + + // Read from the channel for one minute. If the timeout is exceeded and the registration window is still open on the client, the browser reconnects anyway + var uid []byte + ctx, cancel := context.WithTimeout(req.Context(), 1*time.Minute) + defer cancel() + select { + case uid = <-k.card: + case <-ctx.Done(): + http.Error(res, ctx.Err().Error(), http.StatusRequestTimeout) + return + } + + // Send card UID in hexadecimal to client + uidString := hex.EncodeToString(uid) + k.log.Println("Card UID obtained! Card uid is", uidString) + + if _, err := res.Write([]byte("event: card\ndata: " + uidString + "\n\n")); err != nil { + k.log.Println("Could not write: ", err) + } + + if f, ok := res.(http.Flusher); ok { + f.Flush() + } +} + +// PostAddCard creates a new Card for the POSTing user +func (k *Kasse) PostAddCard(res http.ResponseWriter, req *http.Request) { + session, err := k.sessions.Get(req, "nnev-kasse") + if err != nil { + http.Redirect(res, req, "/login.html", 302) + return + } + ui, ok := session.Values["user"] + if !ok { + http.Redirect(res, req, "/login.html", 302) + return + } + + user := ui.(User) + + description := req.FormValue("description") + uidString := req.FormValue("uid") + + renderError := func(message string) { + data := struct { + User *User + Description string + Message string + }{ + User: &user, + Description: description, + Message: message, + } + + if err := ExecuteTemplate(res, TemplateInput{Title: "ccchd Kasse", Body: "add_card.html", Data: &data}); err != nil { + k.log.Println("Could not render template:", err) + http.Error(res, "Internal error", 500) + return + } + } + + if len(uidString) == 0 { + renderError("Please swipe Card to register") + return + } + + uid, err := hex.DecodeString(uidString) + if err != nil { + renderError("Hexadecimal UID could not be decoded") + return + } + + _, err = k.AddCard(uid, &user, description) + if err != nil { + if err == ErrCardExists { + renderError("Card is already registered") + } else { + renderError("Card could not be added") + } + return + } + + http.Redirect(res, req, "/", 302) +} + // Handler returns a http.Handler for the webinterface. func (k *Kasse) Handler() http.Handler { r := mux.NewRouter() @@ -213,5 +358,8 @@ func (k *Kasse) Handler() http.Handler { r.Methods("GET").Path("/logout.html").HandlerFunc(k.GetLogout) r.Methods("GET").Path("/create_user.html").HandlerFunc(k.GetNewUserPage) r.Methods("POST").Path("/create_user.html").HandlerFunc(k.PostNewUserPage) + r.Methods("GET").Path("/add_card.html").HandlerFunc(k.GetAddCard) + r.Methods("POST").Path("/add_card.html").HandlerFunc(k.PostAddCard) + r.Methods("GET").Path("/add_card_event").HandlerFunc(k.AddCardEvent) return r } diff --git a/main.go b/main.go index 18e6226..34970a9 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "net/http" "os" "strings" + "sync" "time" "github.com/Merovius/go-misc/lcd2usb" @@ -42,9 +43,11 @@ type NFCEvent struct { // Kasse collects all state of the application in a central type, to make // parallel testing possible. type Kasse struct { - db *sqlx.DB - log *log.Logger - sessions sessions.Store + db *sqlx.DB + log *log.Logger + sessions sessions.Store + card chan []byte + registration sync.Mutex } // User represents a user in the system (as in the database schema). @@ -87,6 +90,8 @@ const ( // AccountEmpty means the charge was not applied, because there are not // enough funds left in the account. AccountEmpty + // UnknownCard means that the swipe will be ignored + UnknownCard ) // Result is the action taken by a swipe of a card. It contains all information @@ -177,6 +182,20 @@ var ErrWrongAuth = errors.New("wrong username or password") func (k *Kasse) HandleCard(uid []byte) (*Result, error) { k.log.Printf("Card %x was swiped", uid) + // if some routine is reading from the card channel, return nil and no error, since all functionality should be handled by the listening routine. + select { + case k.card <- uid: + k.log.Println("Card UID dumped to other routine") + return &Result{ + Code: UnknownCard, + UID: uid, + User: "", + Account: 0, + }, nil + default: + // do nothing and simply continue with execution + } + tx, err := k.db.Beginx() if err != nil { return nil, err @@ -187,7 +206,13 @@ func (k *Kasse) HandleCard(uid []byte) (*Result, error) { var user User if err := tx.Get(&user, `SELECT users.user_id, name, password FROM cards LEFT JOIN users ON cards.user_id = users.user_id WHERE card_id = $1`, uid); err != nil { k.log.Println("Card not found in database") - return nil, ErrCardNotFound + + return &Result{ + Code: UnknownCard, + UID: uid, + User: "", + Account: 0, + }, ErrCardNotFound } k.log.Printf("Card belongs to %v", user.Name) @@ -210,7 +235,7 @@ func (k *Kasse) HandleCard(uid []byte) (*Result, error) { } if balance < 100 { res.Code = AccountEmpty - return res, ErrAccountEmpty + return res, nil } // Insert new transaction @@ -283,8 +308,8 @@ func (k *Kasse) RegisterUser(name string, password []byte) (*User, error) { // AddCard adds a card to the database with a given owner and returns a // populated card struct. It returns ErrCardExists if a card with the given UID // already exists. -func (k *Kasse) AddCard(uid []byte, owner *User) (*Card, error) { - k.log.Printf("Adding card %x for owner %s", uid, owner.Name) +func (k *Kasse) AddCard(uid []byte, owner *User, description string) (*Card, error) { + k.log.Printf("Adding card %x for owner %s and description %s", uid, owner.Name, description) tx, err := k.db.Beginx() if err != nil { @@ -302,7 +327,7 @@ func (k *Kasse) AddCard(uid []byte, owner *User) (*Card, error) { return nil, err } - if _, err := tx.Exec(`INSERT INTO cards (card_id, user_id, description) VALUES ($1, $2, '')`, uid, owner.ID); err != nil { + if _, err := tx.Exec(`INSERT INTO cards (card_id, user_id, description) VALUES ($1, $2, $3)`, uid, owner.ID, description); err != nil { return nil, err } @@ -314,6 +339,7 @@ func (k *Kasse) AddCard(uid []byte, owner *User) (*Card, error) { card.ID = uid card.User = owner.ID + return &card, nil } @@ -356,6 +382,15 @@ func (k *Kasse) GetCards(user User) ([]Card, error) { return cards, nil } +// GetCard gets the cards for a given card uid and user. +func (k *Kasse) GetCard(uid []byte, user User) (*Card, error) { + var cards []Card + if err := k.db.Select(&cards, `SELECT card_id, user_id, description FROM cards WHERE card_id = $1 AND user_id = $2`, uid, user.ID); err != nil { + return nil, err + } + return &cards[0], nil +} + // GetBalance gets the current balance for a given user. func (k *Kasse) GetBalance(user User) (int64, error) { var b sql.NullInt64 @@ -399,6 +434,8 @@ func main() { } }() + k.card = make(chan []byte) + k.registration = sync.Mutex{} k.sessions = sessions.NewCookieStore([]byte("TODO: Set up safer password")) http.Handle("/", handlers.LoggingHandler(os.Stderr, k.Handler())) @@ -411,6 +448,7 @@ func main() { } events := make(chan NFCEvent) + // We have to wrap the call in a func(), because the go statement evaluates // it's arguments in the current goroutine, and the argument to log.Fatal // blocks in these cases. diff --git a/main_test.go b/main_test.go index e524622..d20c92a 100644 --- a/main_test.go +++ b/main_test.go @@ -79,7 +79,7 @@ func insertData(t *testing.T, db *sqlx.DB, us []User, cs []Card, ts []Transactio func TestHandleCard(t *testing.T) { t.Parallel() - k := Kasse{db: createDB(t), log: testLogger(t)} + k := Kasse{card: make(chan []byte), db: createDB(t), log: testLogger(t)} defer k.db.Close() insertData(t, k.db, []User{ @@ -97,34 +97,40 @@ func TestHandleCard(t *testing.T) { }) tcs := []struct { + reading bool input []byte wantErr error want ResultCode }{ - {[]byte("foobar"), ErrCardNotFound, 0}, - {[]byte("baaa"), ErrAccountEmpty, AccountEmpty}, - {[]byte("baab"), ErrAccountEmpty, AccountEmpty}, - {[]byte("aaaa"), nil, PaymentMade}, - {[]byte("aaab"), nil, PaymentMade}, - {[]byte("aaaa"), nil, PaymentMade}, - {[]byte("aaab"), nil, LowBalance}, - {[]byte("aaab"), nil, LowBalance}, - {[]byte("aaaa"), nil, LowBalance}, - {[]byte("aaab"), nil, LowBalance}, - {[]byte("aaaa"), nil, LowBalance}, - {[]byte("aaab"), ErrAccountEmpty, AccountEmpty}, - {[]byte("aaaa"), ErrAccountEmpty, AccountEmpty}, + {true, []byte("asdf"), nil, UnknownCard}, + {false, []byte("foobar"), ErrCardNotFound, UnknownCard}, + {false, []byte("baaa"), nil, AccountEmpty}, + {false, []byte("baab"), nil, AccountEmpty}, + {false, []byte("aaaa"), nil, PaymentMade}, + {false, []byte("aaab"), nil, PaymentMade}, + {false, []byte("aaaa"), nil, PaymentMade}, + {true, []byte("aaaa"), nil, UnknownCard}, + {false, []byte("aaab"), nil, LowBalance}, + {false, []byte("aaab"), nil, LowBalance}, + {false, []byte("aaaa"), nil, LowBalance}, + {false, []byte("aaab"), nil, LowBalance}, + {false, []byte("aaaa"), nil, LowBalance}, + {false, []byte("aaab"), nil, AccountEmpty}, + {false, []byte("aaaa"), nil, AccountEmpty}, } for _, tc := range tcs { - got, gotErr := k.HandleCard(tc.input) - if tc.wantErr != nil { - if gotErr != tc.wantErr { - t.Errorf("HandleCard(%s) == (%v, %v), want (_, %v)", string(tc.input), got, gotErr, tc.wantErr) - } - continue + if tc.reading { + ch := make(chan bool) + go func() { + ch <- true + <-k.card + }() + // wait for go routine to be ready + <-ch } - if got == nil || got.Code != tc.want { + got, gotErr := k.HandleCard(tc.input) + if gotErr != tc.wantErr || got.Code != tc.want { t.Errorf("HandleCard(%s) == (%v, %v), want (%v, %v)", string(tc.input), got, gotErr, tc.want, tc.wantErr) } } @@ -254,7 +260,7 @@ func TestAddCard(t *testing.T) { } for _, tc := range tcs { - gotCard, gotErr := k.AddCard(tc.uid, tc.user) + gotCard, gotErr := k.AddCard(tc.uid, tc.user, "Description test") if gotErr != tc.wantErr { t.Errorf("AddCard(%x, %v) == (%v, %v), want (_, %v)", tc.uid, tc.user, gotCard, gotErr, tc.wantErr) continue diff --git a/templates/add_card.html b/templates/add_card.html new file mode 100644 index 0000000..10e262e --- /dev/null +++ b/templates/add_card.html @@ -0,0 +1,70 @@ + + +
+
+
+
+

Adding Card for {{ .User.Name }}

+
+
+ Bitte gib eine Beschreibung für + deine Karte ein und geh deine Karte swipen. +
+ {{ .Message }} +
+
+ Waiting for other users to finish registration… +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+
+ + diff --git a/templates/dashboard.html b/templates/dashboard.html index fbb9ff5..9f0f294 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -25,7 +25,7 @@

Registrierte Karten

{{ if .Cards }} - +
@@ -34,7 +34,7 @@

Registrierte Karten

{{ range .Cards }} - + @@ -46,9 +46,11 @@

Registrierte Karten

{{ end }} diff --git a/templates/layout.html b/templates/layout.html index ba489d2..ef22201 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -12,20 +12,22 @@
-
-
- - {{ .Title }} - -
- {{if ne .Title "Login"}} - Logout - {{end}} -
-
-
-
{{ template "content" .Data }}
-
+
+
+ + {{ .Title }} + +
+ {{if ne .Title "Login"}} + Logout + {{end}} +
+
+
+
+ {{ template "content" .Data }} +
+
diff --git a/templates/login.html b/templates/login.html index 7994ff5..e177629 100644 --- a/templates/login.html +++ b/templates/login.html @@ -16,10 +16,10 @@ -
- Neu erstellen -
- -
+
+ Neu erstellen +
+ +
From 54066f7b40bd7fd12ae1fc4606f54933bcef6103 Mon Sep 17 00:00:00 2001 From: Tobias Wackenhut Date: Thu, 27 Feb 2020 23:40:55 +0100 Subject: [PATCH 2/2] add edit and remove card functionality - add backend functions for removing and updating cards - add HTTP handlers for removing and updating cards --- http.go | 163 +++++++++++++++++++++++++++++++++++++++ main.go | 62 +++++++++++++++ templates/dashboard.html | 2 + templates/edit_card.html | 44 +++++++++++ 4 files changed, 271 insertions(+) create mode 100644 templates/edit_card.html diff --git a/http.go b/http.go index 8a62a5f..b2ee30d 100644 --- a/http.go +++ b/http.go @@ -348,6 +348,166 @@ func (k *Kasse) PostAddCard(res http.ResponseWriter, req *http.Request) { http.Redirect(res, req, "/", 302) } +// PostRemoveCard removes a card for the POSTing user +func (k *Kasse) PostRemoveCard(res http.ResponseWriter, req *http.Request) { + session, err := k.sessions.Get(req, "nnev-kasse") + if err != nil { + http.Redirect(res, req, "/login.html", 302) + return + } + ui, ok := session.Values["user"] + if !ok { + http.Redirect(res, req, "/login.html", 302) + return + } + + user := ui.(User) + + err = req.ParseForm() + if err != nil { + http.Error(res, "Internal error", http.StatusBadRequest) + } + uidString := req.Form.Get("uid") + + uid, err := hex.DecodeString(uidString) + if err != nil { + http.Redirect(res, req, "/", http.StatusNotFound) + return + } + + card, err := k.GetCard(uid, user) + if err != nil { + http.Redirect(res, req, "/", http.StatusNotFound) + return + } + + err = k.RemoveCard(uid, &user) + if err != nil { + data := struct { + Card *Card + Message string + }{ + Card: card, + Message: err.Error(), + } + + if err := ExecuteTemplate(res, TemplateInput{Title: "ccchd Kasse", Body: "edit_card.html", Data: &data}); err != nil { + k.log.Println("Could not render template:", err) + http.Error(res, "Internal error", 500) + return + } + } else { + http.Redirect(res, req, "/", http.StatusFound) + return + } + + http.Redirect(res, req, "/", 302) +} + +// PostEditCard renders an edit dialog for a given card +func (k *Kasse) PostEditCard(res http.ResponseWriter, req *http.Request) { + session, err := k.sessions.Get(req, "nnev-kasse") + if err != nil { + http.Redirect(res, req, "/login.html", 302) + return + } + ui, ok := session.Values["user"] + if !ok { + http.Redirect(res, req, "/login.html", 302) + return + } + + user := ui.(User) + + uids, ok := req.URL.Query()["uid"] + if !ok || len(uids[0]) < 1 { + http.Error(res, "No uid given", http.StatusBadRequest) + return + } + uidString := uids[0] + + uid, err := hex.DecodeString(uidString) + if err != nil { + http.Redirect(res, req, "/", http.StatusNotFound) + return + } + + card, err := k.GetCard(uid, user) + if err != nil { + http.Redirect(res, req, "/", http.StatusNotFound) + return + } + + data := struct { + Card *Card + Message string + }{ + Card: card, + Message: "", + } + + if err := ExecuteTemplate(res, TemplateInput{Title: "ccchd Kasse", Body: "edit_card.html", Data: &data}); err != nil { + k.log.Println("Could not render template:", err) + http.Error(res, "Internal error", 500) + return + } +} + +// PostUpdateCard saves card changes and redirects to the dashboard +func (k *Kasse) PostUpdateCard(res http.ResponseWriter, req *http.Request) { + session, err := k.sessions.Get(req, "nnev-kasse") + if err != nil { + http.Redirect(res, req, "/login.html", 302) + return + } + ui, ok := session.Values["user"] + if !ok { + http.Redirect(res, req, "/login.html", 302) + return + } + + user := ui.(User) + + err = req.ParseForm() + if err != nil { + http.Error(res, "Internal error", http.StatusBadRequest) + } + uidString := req.Form.Get("uid") + + uid, err := hex.DecodeString(uidString) + if err != nil { + http.Redirect(res, req, "/", http.StatusNotFound) + } + + description := req.Form.Get("description") + + err = k.UpdateCard(uid, &user, description) + if err != nil { + card, err2 := k.GetCard(uid, user) + message := err.Error() + if err2 != nil { + message = err2.Error() + } + + data := struct { + Card *Card + Message string + }{ + Card: card, + Message: message, + } + + if err := ExecuteTemplate(res, TemplateInput{Title: "ccchd Kasse", Body: "edit_card.html", Data: &data}); err != nil { + k.log.Println("Could not render template:", err) + http.Error(res, "Internal error", 500) + return + } + } + + http.Redirect(res, req, "/", http.StatusFound) + return +} + // Handler returns a http.Handler for the webinterface. func (k *Kasse) Handler() http.Handler { r := mux.NewRouter() @@ -361,5 +521,8 @@ func (k *Kasse) Handler() http.Handler { r.Methods("GET").Path("/add_card.html").HandlerFunc(k.GetAddCard) r.Methods("POST").Path("/add_card.html").HandlerFunc(k.PostAddCard) r.Methods("GET").Path("/add_card_event").HandlerFunc(k.AddCardEvent) + r.Methods("POST").Path("/remove_card.html").HandlerFunc(k.PostRemoveCard) + r.Methods("GET").Path("/edit_card.html").HandlerFunc(k.PostEditCard) + r.Methods("POST").Path("/update_card.html").HandlerFunc(k.PostUpdateCard) return r } diff --git a/main.go b/main.go index 34970a9..86adbae 100644 --- a/main.go +++ b/main.go @@ -343,6 +343,68 @@ func (k *Kasse) AddCard(uid []byte, owner *User, description string) (*Card, err return &card, nil } +// RemoveCard removes a card. The function checks, if the requesting user is the card owner and prevents removal otherwise. It takes the UID of the card to remove and returns ErrCardNotFound if the card was not found or does not belong to the requesting user +func (k *Kasse) RemoveCard(uid []byte, user *User) error { + k.log.Printf("Removing card %x", uid) + + tx, err := k.db.Beginx() + if err != nil { + return err + } + defer tx.Rollback() + + // We need to check first if the card actually belongs to the user, which wants to remove it + var card Card + if err := tx.Get(&card, `SELECT card_id, user_id FROM cards WHERE card_id = $1 AND user_id = $2`, uid, user.ID); err == sql.ErrNoRows { + return ErrCardNotFound + } else if err != nil { + return err + } + + if _, err := tx.Exec(`DELETE FROM cards WHERE card_id == $1 AND user_id == $2`, card.ID, user.ID); err != nil { + return err + } + + if err := tx.Commit(); err != nil { + return err + } + + k.log.Println("Card removed successfully") + + return nil +} + +// UpdateCard updates the description of a card. It takes a uid, a user and a description and returns an error +func (k *Kasse) UpdateCard(uid []byte, user *User, description string) error { + k.log.Printf("Updating card %x", uid) + + tx, err := k.db.Beginx() + if err != nil { + return err + } + defer tx.Rollback() + + // We need to check first if the card actually belongs to the user, which wants to remove it + var card Card + if err := tx.Get(&card, `SELECT card_id, user_id FROM cards WHERE card_id = $1 AND user_id = $2`, uid, user.ID); err == sql.ErrNoRows { + return ErrCardNotFound + } else if err != nil { + return err + } + + if _, err := tx.Exec(`UPDATE cards SET description = $1 WHERE card_id == $2 AND user_id == $3`, description, card.ID, user.ID); err != nil { + return err + } + + if err := tx.Commit(); err != nil { + return err + } + + k.log.Println("Card updated successfully") + + return nil +} + // Authenticate tries to authenticate a given username/password combination // against the database. It is guaranteed to take at least 200 Milliseconds. It // returns ErrWrongAuth, if the user or password was wrong. If no error diff --git a/templates/dashboard.html b/templates/dashboard.html index 9f0f294..6903e50 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -30,6 +30,7 @@

Registrierte Karten

+ @@ -37,6 +38,7 @@

Registrierte Karten

+ {{ end }} diff --git a/templates/edit_card.html b/templates/edit_card.html new file mode 100644 index 0000000..42c26b3 --- /dev/null +++ b/templates/edit_card.html @@ -0,0 +1,44 @@ + + +
+
+
+
+

Edit Card {{ printf "%x" .Card.ID }}

+
+
+

{{ .Message }}

+
+
+
+
+ + + +
+
+
+ + + +
+ +
+
+
+
+
UID
{{ printf "%x" .ID }} {{ .Description }}
UID BezeichnungBearbeiten
{{ printf "%x" .ID }} {{ .Description }}edit