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 +
+ +
UID
{{ printf "%x" .ID }} {{ .Description }}