forked from nnev/kasse
-
Notifications
You must be signed in to change notification settings - Fork 2
/
main.go
437 lines (379 loc) · 11.9 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
package main
import (
"database/sql"
"encoding/gob"
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/gorilla/context"
"github.com/gorilla/handlers"
"github.com/gorilla/sessions"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"golang.org/x/crypto/bcrypt"
"merovius.de/go-misc/lcd2usb"
)
var (
// Defaults for development
driver = flag.String("sql-driver", "sqlite3", "The SQL driver to use for the database")
connect = flag.String("connect", "kasse.sqlite", "The connection specification for the database")
listen = flag.String("listen", "localhost:9000", "Where to listen for HTTP connections")
hardware = flag.Bool("hardware", true, "Whether hardware is plugged in")
)
func init() {
gob.Register(User{})
}
// 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
}
// User represents a user in the system (as in the database schema).
type User struct {
ID int `db:"user_id"`
Name string `db:"name"`
Password []byte `db:"password"`
}
// Card represents a card in the system (as in the database schema).
type Card struct {
ID []byte `db:"card_id"`
User int `db:"user_id"`
Description string `db:"description"`
}
// Transaction represents a transaction in the system (as in the database
// schema).
type Transaction struct {
ID int `db:"transaction_id"`
User int `db:"user_id"`
Card []byte `db:"card_id"`
Time time.Time `db:"time"`
Amount int `db:"amount"`
Kind string `db:"kind"`
}
// ResultCode is the action taken by a swipe of a card. It should be
// communicated to the user.
type ResultCode int
const (
_ ResultCode = iota
// PaymentMade means the charge was applied successfully and there are
// sufficient funds left in the account.
PaymentMade
// LowBalance means the charge was applied successfully, but the account is
// nearly empty and should be recharged soon.
LowBalance
// AccountEmpty means the charge was not applied, because there are not
// enough funds left in the account.
AccountEmpty
)
// Result is the action taken by a swipe of a card. It contains all information
// to be communicated to the user.
type Result struct {
Code ResultCode
UID []byte
User string
Account float32
}
func flashLCD(lcd *lcd2usb.Device, text string, r, g, b uint8) error {
lcd.Color(r, g, b)
for i, l := range strings.Split(text, "\n") {
if len(l) > 16 {
l = l[:16]
}
if i > 2 {
break
}
lcd.CursorPosition(1, uint8(i+1))
fmt.Fprint(lcd, l)
}
// TODO: Make flag
time.Sleep(time.Second)
lcd.Color(0, 0, 255)
lcd.Clear()
return nil
}
// Print writes the result to a 16x2 LCD display.
func (res *Result) Print(lcd *lcd2usb.Device) error {
var r, g, b uint8
// TODO(mero): Make sure format does not overflow (floating point)
text := fmt.Sprintf("Card: %x\n%-9s%.2fE", res.UID, res.User, res.Account)
switch res.Code {
default:
r, g, b = 255, 255, 255
case PaymentMade:
r, g, b = 0, 255, 0
case LowBalance:
r, g, b = 255, 50, 0
case AccountEmpty:
r, g, b = 255, 0, 0
}
return flashLCD(lcd, text, r, g, b)
}
// String implements fmt.Stringer.
func (r ResultCode) String() string {
switch r {
case PaymentMade:
return "PaymentMade"
case LowBalance:
return "LowBalance"
case AccountEmpty:
return "AccountEmpty"
default:
return fmt.Sprintf("Result(%d)", r)
}
}
// ErrAccountEmpty means the charge couldn't be applied because there where
// insufficient funds.
var ErrAccountEmpty = errors.New("account is empty")
// ErrCardNotFound means the charge couldn't be applied becaus the card is not
// registered to any user.
var ErrCardNotFound = errors.New("card not found")
// ErrUserExists means that a duplicate username was tried to register.
var ErrUserExists = errors.New("username already taken")
// ErrCardExists means that an already registered card was tried to register
// again.
var ErrCardExists = errors.New("card already registered")
// ErrWrongAuth means that an invalid username or password was provided for
// Authentication.
var ErrWrongAuth = errors.New("wrong username or password")
// HandleCard handles the swiping of a new card. It looks up the user the card
// belongs to and checks the account balance. It returns PaymentMade, when the
// account has been charged correctly, LowBalance if there is less than 5€ left
// after the charge (the charge is still made) and AccountEmpty when there is
// no balance left on the account. The account is charged if and only if the
// returned error is nil.
func (k *Kasse) HandleCard(uid []byte) (*Result, error) {
k.log.Printf("Card %x was swiped", uid)
tx, err := k.db.Beginx()
if err != nil {
return nil, err
}
defer tx.Rollback()
// Get user this card belongs to
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
}
k.log.Printf("Card belongs to %v", user.Name)
// Get account balance of this user
var balance int64
var b sql.NullInt64
if err := tx.Get(&b, `SELECT SUM(amount) FROM transactions WHERE user_id = $1`, user.ID); err != nil {
k.log.Println("Could not get balance:", err)
return nil, err
}
if b.Valid {
balance = b.Int64
}
k.log.Printf("Account balance is %d", balance)
res := &Result{
UID: uid,
User: user.Name,
Account: float32(balance) / 100,
}
if balance < 100 {
res.Code = AccountEmpty
return res, ErrAccountEmpty
}
// Insert new transaction
if _, err := tx.Exec(`INSERT INTO transactions (user_id, card_id, time, amount, kind) VALUES ($1, $2, $3, $4, $5)`, user.ID, uid, time.Now(), -100, "Kartenswipe"); err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
if balance < 600 {
k.log.Println("balance is low")
res.Code = LowBalance
} else {
res.Code = PaymentMade
}
k.log.Println("returning")
return res, nil
}
// RegisterUser creates a new row in the user table, with the given username
// and password. It returns a populated User and no error on success. If the
// username is already taken, it returns ErrUserExists.
func (k *Kasse) RegisterUser(name string, password []byte) (*User, error) {
k.log.Printf("Registering user %s", name)
tx, err := k.db.Beginx()
if err != nil {
return nil, err
}
defer tx.Rollback()
pwhash, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// We need to check first if the username is already taken, because the
// error from an insert can't be checked programmatically.
var user User
if err := tx.Get(&user, `SELECT user_id, name, password FROM users WHERE name = $1`, name); err == nil {
return nil, ErrUserExists
} else if err != sql.ErrNoRows {
return nil, err
}
result, err := tx.Exec(`INSERT INTO users (name, password) VALUES ($1, $2)`, name, pwhash)
if err != nil {
return nil, err
}
id, err := result.LastInsertId()
if err != nil {
if err := tx.Get(&id, `SELECT user_id FROM users WHERE name = $1`, name); err != nil {
return nil, err
}
}
if err := tx.Commit(); err != nil {
return nil, err
}
user.ID = int(id)
user.Name = name
user.Password = pwhash
return &user, nil
}
// 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)
tx, err := k.db.Beginx()
if err != nil {
return nil, err
}
defer tx.Rollback()
// We need to check first if the card already exists, because the error
// from an insert can't be checked programatically.
var card Card
if err := tx.Get(&card, `SELECT card_id, user_id FROM cards WHERE card_id = $1`, uid); err == nil {
k.log.Println("Card already exists, current owner", card.User)
return nil, ErrCardExists
} else if err != sql.ErrNoRows {
return nil, err
}
if _, err := tx.Exec(`INSERT INTO cards (card_id, user_id, description) VALUES ($1, $2, '')`, uid, owner.ID); err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
k.log.Println("Card added successfully")
card.ID = uid
card.User = owner.ID
return &card, 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
// occured, it will return a fully populated User.
func (k *Kasse) Authenticate(username string, password []byte) (*User, error) {
k.log.Printf("Verifying user %v", username)
delay := time.After(200 * time.Millisecond)
defer func() {
<-delay
}()
user := new(User)
if err := k.db.Get(user, `SELECT user_id, name, password FROM users WHERE name = $1`, username); err == sql.ErrNoRows {
k.log.Printf("No such user %v", username)
return nil, ErrWrongAuth
} else if err != nil {
return nil, err
}
if err := bcrypt.CompareHashAndPassword(user.Password, password); err == bcrypt.ErrMismatchedHashAndPassword {
k.log.Println("Wrong password")
return nil, ErrWrongAuth
} else if err != nil {
return nil, err
}
k.log.Printf("Successfully authenticated %v", username)
return user, nil
}
// GetCards gets all cards for a given user.
func (k *Kasse) GetCards(user User) ([]Card, error) {
var cards []Card
if err := k.db.Select(&cards, `SELECT card_id, user_id, description FROM cards WHERE user_id = $1`, user.ID); err != nil {
return nil, err
}
return cards, nil
}
// GetBalance gets the current balance for a given user.
func (k *Kasse) GetBalance(user User) (int64, error) {
var b sql.NullInt64
if err := k.db.Get(&b, `SELECT SUM(amount) FROM transactions WHERE user_id = $1`, user.ID); err != nil {
k.log.Println("Could not get balance:", err)
return 0, err
}
return b.Int64, nil
}
// GetTransactions gets the last n transactions for a given user. If n ≤ 0, all
// transactions are returnsed.
func (k *Kasse) GetTransactions(user User, n int) ([]Transaction, error) {
var transactions []Transaction
var err error
if n <= 0 {
err = k.db.Select(&transactions, `SELECT user_id, card_id, time, amount, kind FROM transactions WHERE user_id = $1 ORDER BY time DESC`, user.ID)
} else {
err = k.db.Select(&transactions, `SELECT user_id, card_id, time, amount, kind FROM transactions WHERE user_id = $1 ORDER BY time DESC LIMIT $2`, user.ID, n)
}
if err != nil {
return nil, err
}
return transactions, nil
}
func main() {
flag.Parse()
k := new(Kasse)
k.log = log.New(os.Stderr, "", log.LstdFlags)
if db, err := sqlx.Connect(*driver, *connect); err != nil {
log.Fatal("Could not open database:", err)
} else {
k.db = db
}
defer func() {
if err := k.db.Close(); err != nil {
log.Println("Error closing database:", err)
}
}()
k.sessions = sessions.NewCookieStore([]byte("TODO: Set up safer password"))
http.Handle("/", handlers.LoggingHandler(os.Stderr, k.Handler()))
var lcd *lcd2usb.Device
if *hardware {
var err error
if lcd, err = lcd2usb.Open("/dev/ttyACM0", 2, 16); err != nil {
log.Fatal(err)
}
}
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.
if *hardware {
go func() {
log.Fatal(ConnectAndPollNFCReader("", events))
}()
}
RegisterHTTPReader(k)
go func() {
log.Fatal(http.ListenAndServe(*listen, context.ClearHandler(http.DefaultServeMux)))
}()
for {
ev := <-events
if ev.Err != nil {
log.Println(ev.Err)
continue
}
res, err := k.HandleCard(ev.UID)
if res != nil {
res.Print(lcd)
} else {
// TODO: Distinguish between user-facing errors and internal errors
flashLCD(lcd, err.Error(), 255, 0, 0)
}
}
}