Skip to content

Commit

Permalink
add AddCard functionality
Browse files Browse the repository at this point in the history
- Add channel to Kasse struct to hand over UIDs to registration
- Add HTTP handlers for adding cards
  • Loading branch information
tynsh committed Feb 27, 2020
1 parent 3483400 commit 2efbffd
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 53 deletions.
152 changes: 150 additions & 2 deletions http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand All @@ -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
}
54 changes: 46 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"os"
"strings"
"sync"
"time"

"github.com/Merovius/go-misc/lcd2usb"
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}

Expand All @@ -314,6 +339,7 @@ func (k *Kasse) AddCard(uid []byte, owner *User) (*Card, error) {

card.ID = uid
card.User = owner.ID

return &card, nil
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()))

Expand All @@ -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.
Expand Down
50 changes: 28 additions & 22 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 2efbffd

Please sign in to comment.