From faadbe0c627647d025217f96cf5e1c2fb8e68280 Mon Sep 17 00:00:00 2001
From: Alexis Poyen <apoyen@mail.apoyen.fr>
Date: Tue, 24 Mar 2020 17:49:50 +0100
Subject: [PATCH] Feat : manage bank account without authent

---
 go.mod                         |  11 +-
 go.sum                         |  41 ++++--
 internal/models/models.go      | 228 +++++++++++++++++++++++++++++++++
 internal/models/models_test.go | 105 +++++++++++++++
 internal/rootmux/rootmux.go    |   9 ++
 pkg/glob/LICENSE               |  21 +++
 pkg/glob/glob.go               |  56 ++++++++
 pkg/glob/glob_test.go          | 105 +++++++++++++++
 pkg/tester/tester.go           |   2 +-
 testdata/static/index.html     |  30 -----
 web/index.html                 |   2 +-
 11 files changed, 562 insertions(+), 48 deletions(-)
 create mode 100644 internal/models/models.go
 create mode 100644 internal/models/models_test.go
 create mode 100644 pkg/glob/LICENSE
 create mode 100644 pkg/glob/glob.go
 create mode 100644 pkg/glob/glob_test.go
 delete mode 100644 testdata/static/index.html

diff --git a/go.mod b/go.mod
index 4a1a0da..df131be 100644
--- a/go.mod
+++ b/go.mod
@@ -4,14 +4,15 @@ go 1.14
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/golang/protobuf v1.3.4 // indirect
+	github.com/golang/protobuf v1.3.5 // indirect
+	github.com/jinzhu/gorm v1.9.12
 	github.com/kr/pretty v0.1.0 // indirect
+	github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
 	github.com/oschwald/maxminddb-golang v1.6.0
-	github.com/secure-io/sio-go v0.3.0
-	golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d
-	golang.org/x/net v0.0.0-20200226121028-0de0cce0169b
+	golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6
+	golang.org/x/net v0.0.0-20200319234117-63522dbf7eec // indirect
 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
-	golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae
+	golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d // indirect
 	google.golang.org/appengine v1.6.5 // indirect
 	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 )
diff --git a/go.sum b/go.sum
index c978160..a32cd92 100644
--- a/go.sum
+++ b/go.sum
@@ -2,35 +2,54 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
+github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
+github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
+github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk=
-github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
+github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
+github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
+github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
+github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/oschwald/maxminddb-golang v1.6.0 h1:KAJSjdHQ8Kv45nFIbtoLGrGWqHFajOIm7skTyz/+Dls=
 github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/secure-io/sio-go v0.3.0 h1:QKGb6rGJeiExac9wSWxnWPYo8O8OFN7lxXQvHshX6vo=
-github.com/secure-io/sio-go v0.3.0/go.mod h1:D3KmXgKETffyYxBdFRN+Hpd2WzhzqS0EQwT3XWsAcBU=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
-golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6 h1:TjszyFsQsyZNHwdVdZ5m7bjmreu0znc2kRYsEml9/Ww=
+golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200319234117-63522dbf7eec h1:w0SItUiQ4sBiXBAwWNkyu8Fu2Qpn/dtDIcoPkPDqjRw=
+golang.org/x/net v0.0.0-20200319234117-63522dbf7eec/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -38,8 +57,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d h1:62ap6LNOjDU6uGmKXHJbSfciMoV+FeI1sRXx/pLDL44=
+golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
diff --git a/internal/models/models.go b/internal/models/models.go
new file mode 100644
index 0000000..272556b
--- /dev/null
+++ b/internal/models/models.go
@@ -0,0 +1,228 @@
+package models
+
+import (
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+
+	"encoding/json"
+
+	"github.com/jinzhu/gorm"
+	// Needed for sqlite
+
+	_ "github.com/jinzhu/gorm/dialects/sqlite"
+)
+
+// DataHandler init a gorm DB an presents API handlers
+type DataHandler struct {
+	db *gorm.DB
+}
+
+// NewDataHandler init a DataHandler and returns a pointer to it
+func NewDataHandler() *DataHandler {
+	db, err := gorm.Open("sqlite3", "../../data/test.db")
+	if err != nil {
+		panic("failed to connect database")
+	}
+	db.LogMode(true)
+
+	db.Model(&Operation{}).AddForeignKey("creditor", "bank_account(id)", "RESTRICT", "RESTRICT")
+
+	// Migrate the schema
+	db.AutoMigrate(&UserClient{})
+	db.AutoMigrate(&UserBanker{})
+	db.AutoMigrate(&BankAccount{})
+	db.AutoMigrate(&Operation{})
+	return &DataHandler{db: db}
+}
+
+// UserClient has many BankAccounts, UserClientID is the foreign key
+type UserClient struct {
+	ID           uint       `gorm:"primary_key"`
+	CreatedAt    time.Time  `json:"-"`
+	UpdatedAt    time.Time  `json:"-"`
+	DeletedAt    *time.Time `json:"-"`
+	Name         string
+	BankAccounts []BankAccount
+}
+
+type UserBanker struct {
+	ID          uint       `gorm:"primary_key"`
+	CreatedAt   time.Time  `json:"-"`
+	UpdatedAt   time.Time  `json:"-"`
+	DeletedAt   *time.Time `json:"-"`
+	Name        string
+	UserClients []UserClient
+}
+
+// BankAccount belongs to an UserClient
+type BankAccount struct {
+	ID            uint       `gorm:"primary_key"`
+	CreatedAt     time.Time  `json:"-"`
+	UpdatedAt     time.Time  `json:"-"`
+	DeletedAt     *time.Time `json:"-"`
+	Number        string     `gorm:"primary_key"`
+	UserClientID  uint
+	Type          string
+	Amount        int
+	BankOverdraft int
+	Operations    []Operation `gorm:"foreignkey:Debtor"`
+}
+
+type Operation struct {
+	ID        uint `gorm:"primary_key"`
+	Debtor    uint
+	CreatedAt time.Time  `json:"-"`
+	UpdatedAt time.Time  `json:"-"`
+	DeletedAt *time.Time `json:"-"`
+	Amount    int
+	Date      time.Time
+	Creditor  uint
+}
+
+// HandleClients expose the UserClients API
+func (d *DataHandler) HandleClients(w http.ResponseWriter, r *http.Request) {
+	id, _ := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/api/UserClients/"))
+	switch method := r.Method; method {
+	case "GET":
+		if id != 0 {
+			var o UserClient
+			if err := d.db.Preload("BankAccounts").First(&o, id).Error; err != nil {
+				http.Error(w, "id does not exist", http.StatusNotFound)
+				return
+			}
+			json.NewEncoder(w).Encode(o)
+		} else {
+			var o []UserClient
+			d.db.Preload("BankAccounts").Find(&o)
+			json.NewEncoder(w).Encode(o)
+		}
+	case "POST":
+		var o UserClient
+		err := json.NewDecoder(r.Body).Decode(&o)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+		d.db.Create(&o)
+	case "DELETE":
+		if id != 0 {
+			var o UserClient
+			if err := d.db.First(&o, id).Error; err != nil {
+				http.Error(w, "id does not exist", http.StatusNotFound)
+				return
+			}
+			d.db.Delete(&o)
+		} else {
+			http.Error(w, "id is missing", http.StatusNotFound)
+		}
+	default:
+		http.Error(w, "method not allowed", 400)
+	}
+}
+
+func (d *DataHandler) HandleBankAccounts(w http.ResponseWriter, r *http.Request) {
+	id, _ := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/api/BankAccounts/"))
+	switch method := r.Method; method {
+	case "GET":
+		if id != 0 {
+			var o BankAccount
+			if err := d.db.Preload("Operations").First(&o, id).Error; err != nil {
+				http.Error(w, "id does not exist", http.StatusNotFound)
+				return
+			}
+			json.NewEncoder(w).Encode(o)
+		} else {
+			var o []BankAccount
+			d.db.Preload("Operations").Find(&o)
+			json.NewEncoder(w).Encode(o)
+		}
+	case "POST":
+		var o BankAccount
+		err := json.NewDecoder(r.Body).Decode(&o)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+		d.db.Create(&o)
+	case "DELETE":
+		if id != 0 {
+			var o BankAccount
+			if err := d.db.First(&o, id).Error; err != nil {
+				http.Error(w, "id does not exist", http.StatusNotFound)
+				return
+			}
+			d.db.Delete(&o)
+		} else {
+			http.Error(w, "id is missing", http.StatusNotFound)
+		}
+	default:
+		http.Error(w, "method not allowed", 400)
+	}
+}
+
+func (d *DataHandler) HandleOperations(w http.ResponseWriter, r *http.Request) {
+	id, _ := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/api/Operations/"))
+	switch method := r.Method; method {
+	case "GET":
+		if id != 0 {
+			var o Operation
+			if err := d.db.First(&o, id).Error; err != nil {
+				http.Error(w, "id does not exist", http.StatusNotFound)
+				return
+			}
+			json.NewEncoder(w).Encode(o)
+		} else {
+			var o []Operation
+			d.db.Find(&o)
+			json.NewEncoder(w).Encode(o)
+		}
+	case "POST":
+		var o Operation
+		err := json.NewDecoder(r.Body).Decode(&o)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+
+		var debtor BankAccount
+		var creditor BankAccount
+		if err := d.db.First(&debtor, o.Debtor).Error; err == nil {
+			if (debtor.Amount + o.Amount) >= debtor.BankOverdraft {
+				if err := d.db.First(&creditor, o.Creditor).Error; err == nil {
+					// Update BankAccounts
+					debtor.Amount += o.Amount
+					creditor.Amount -= o.Amount
+					d.db.Save(&debtor)
+					d.db.Save(&creditor)
+
+					now := time.Now()
+					o.Date = now
+					d.db.Create(&o)
+
+					// Add the operation to creditor
+					op := Operation{
+						Debtor:   o.Creditor,
+						Amount:   o.Amount,
+						Date:     now,
+						Creditor: o.Debtor,
+					}
+					d.db.Create(&op)
+				}
+			} else {
+				http.Error(w, "Not enough money", http.StatusExpectationFailed)
+			}
+		}
+	case "DELETE":
+		if id != 0 {
+			var o Operation
+			if err := d.db.First(&o, id).Error; err != nil {
+				http.Error(w, "id does not exist", http.StatusNotFound)
+				return
+			}
+			d.db.Delete(&o)
+		} else {
+			http.Error(w, "id is missing", http.StatusNotFound)
+		}
+	default:
+		http.Error(w, "method not allowed", 400)
+	}
+}
diff --git a/internal/models/models_test.go b/internal/models/models_test.go
new file mode 100644
index 0000000..e76f4c1
--- /dev/null
+++ b/internal/models/models_test.go
@@ -0,0 +1,105 @@
+package models
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"os"
+	"testing"
+
+	"github.com/nicolaspernoud/vestibule/pkg/tester"
+)
+
+func TestHandleUserClients(t *testing.T) {
+	// Remove the test db to start clean
+	os.Remove("../../data/test.db")
+	// Create the handler
+	dh := NewDataHandler()
+	ts := httptest.NewServer(http.HandlerFunc(dh.HandleClients))
+	defer ts.Close()
+	url, _ := url.Parse(ts.URL)
+	port := url.Port()
+	// Wrap the testing function
+	do := tester.CreateServerTester(t, port, "sdk-go.io", nil)
+	noH := tester.Header{Key: "", Value: ""}
+
+	// Try to create a client
+	do("POST", "/api/UserClients", noH, `{"Name":"Dupond"}`, 200, "")
+	// Try to create an other one
+	do("POST", "/api/UserClients", noH, `{"Name":"Boulangerie"}`, 200, "")
+	// Try to get the first client
+	do("GET", "/api/UserClients/1", noH, "", 200, `{"ID":1,"Name":"Dupond","BankAccounts":[]}`)
+	// // Try to get all the clients
+	do("GET", "/api/UserClients", noH, "", 200, `[{"ID":1,"Name":"Dupond","BankAccounts":[]},{"ID":2,"Name":"Boulangerie","BankAccounts":[]}]`)
+}
+
+func TestHandleBankAccounts(t *testing.T) {
+	// Create the handler
+	dh := NewDataHandler()
+	ts := httptest.NewServer(http.HandlerFunc(dh.HandleBankAccounts))
+	defer ts.Close()
+	url, _ := url.Parse(ts.URL)
+	port := url.Port()
+	// Wrap the testing function
+	do := tester.CreateServerTester(t, port, "sdk-go.io", nil)
+	noH := tester.Header{Key: "", Value: ""}
+
+	// Add bank account to client
+	do("POST", "/api/BankAccounts", noH, `{"Number":"01-01","UserClientID":1,"Type":"checking-account","Amount":458,"BankOverdraft":-100}`, 200, "")
+	// Add an other bank account to client
+	do("POST", "/api/BankAccounts", noH, `{"Number":"01-02","UserClientID":1,"Type":"saving-account","Amount":1287,"BankOverdraft":0}`, 200, "")
+	// Get account where id=1
+	do("GET", "/api/BankAccounts/1", noH, "", 200, `{"ID":1,"Number":"01-01","UserClientID":1,"Type":"checking-account","Amount":458,"BankOverdraft":-100,"Operations":[]}`)
+	// Get all Bank account
+	do("GET", "/api/BankAccounts/", noH, "", 200, `[{"ID":1,"Number":"01-01","UserClientID":1,"Type":"checking-account","Amount":458,"BankOverdraft":-100,"Operations":[]},{"ID":2,"Number":"01-02","UserClientID":1,"Type":"saving-account","Amount":1287,"BankOverdraft":0,"Operations":[]}]`)
+	// Add a bank account to Bakery
+	do("POST", "/api/BankAccounts", noH, `{"Number":"02-01","UserClientID":2,"Type":"checking-account","Amount":4745,"BankOverdraft":-500}`, 200, "")
+
+	// Try to delete both BankAccount
+	// do("DELETE", "/api/BankAccounts/1", noH, ``, 200, "")
+	// do("DELETE", "/api/BankAccounts/2", noH, ``, 200, "")
+}
+
+func TestHandleOperations(t *testing.T) {
+	// Create the handler
+	dh := NewDataHandler()
+	ts := httptest.NewServer(http.HandlerFunc(dh.HandleOperations))
+	defer ts.Close()
+	url, _ := url.Parse(ts.URL)
+	port := url.Port()
+	// Wrap the testing function
+	do := tester.CreateServerTester(t, port, "sdk-go.io", nil)
+	noH := tester.Header{Key: "", Value: ""}
+
+	// Add operation between client and Bakery
+	do("POST", "/api/Operations", noH, `{"Debtor":1,"Amount":-100,"Creditor":3}`, 200, "")
+	// Get operation where id=1
+	do("GET", "/api/Operations/1", noH, "", 200, `{"ID":1,"Debtor":1,"Amount":-100`)
+	// Get all Bank account
+	do("GET", "/api/Operations/", noH, "", 200, `[{"ID":1,"Debtor":1,"Amount":-100`)
+	// Add invalid operation between client and Bakery must be refused with 417 (Expectation failed)
+	do("POST", "/api/Operations", noH, `{"Debtor":1,"Amount":-1789,"Creditor":3}`, 417, "Not enough money")
+
+}
+
+func TestHandleUserClientWithAccounts(t *testing.T) {
+	// Create the handler
+	dh := NewDataHandler()
+	ts := httptest.NewServer(http.HandlerFunc(dh.HandleClients))
+	defer ts.Close()
+	url, _ := url.Parse(ts.URL)
+	port := url.Port()
+	// Wrap the testing function
+	do := tester.CreateServerTester(t, port, "sdk-go.io", nil)
+	noH := tester.Header{Key: "", Value: ""}
+
+	// Get client Dupond with his banks accounts and operations up to date (-100€ on checking-account)
+	do("GET", "/api/UserClients/1", noH, "", 200, `{"ID":1,"Name":"Dupond","BankAccounts":[{"ID":1,"Number":"01-01","UserClientID":1,"Type":"checking-account","Amount":358,"BankOverdraft":-100,"Operations":null},{"ID":2,"Number":"01-02","UserClientID":1,"Type":"saving-account","Amount":1287,"BankOverdraft":0,"Operations":null}]}`)
+	// Get client Bakery with his banks accounts and operations up to date (+100€ on checking-account)
+	do("GET", "/api/UserClients/2", noH, "", 200, `{"ID":2,"Name":"Boulangerie","BankAccounts":[{"ID":3,"Number":"02-01","UserClientID":2,"Type":"checking-account","Amount":4845,"BankOverdraft":-500,"Operations":null}]}`)
+
+	// // Try to delete the above created user
+	// do("DELETE", "/api/UserBanks/1", noH, ``, 200, "")
+	// // Try to get the first user again
+	// do("GET", "/api/UserBanks/1", noH, "", 404, `id does not exist`)
+}
diff --git a/internal/rootmux/rootmux.go b/internal/rootmux/rootmux.go
index 92aa5e6..586d5b0 100644
--- a/internal/rootmux/rootmux.go
+++ b/internal/rootmux/rootmux.go
@@ -4,6 +4,7 @@ import (
 	"net/http"
 	"os"
 
+	"github.com/nicolaspernoud/vestibule/internal/models"
 	"github.com/nicolaspernoud/vestibule/pkg/auth"
 	"github.com/nicolaspernoud/vestibule/pkg/middlewares"
 
@@ -28,6 +29,14 @@ func CreateRootMux(port int, staticDir string) RootMux {
 	mainMux.Handle("/OAuth2Callback", m.HandleOAuth2Callback())
 	mainMux.HandleFunc("/Logout", m.HandleLogout)
 	mainMux.HandleFunc("/Login", m.HandleInMemoryLogin)
+
+	// Bank API endpoints
+	dh := models.NewDataHandler()
+	mainMux.HandleFunc("/api/UserClients/", dh.HandleClients)
+	mainMux.HandleFunc("/api/BankAccounts/", dh.HandleBankAccounts)
+	mainMux.HandleFunc("/api/Operations/", dh.HandleOperations)
+
+	// Common API endpoints
 	commonMux := http.NewServeMux()
 	mainMux.Handle("/api/common/WhoAmI", auth.ValidateAuthMiddleware(auth.WhoAmI(), []string{"*"}, false))
 	commonMux.HandleFunc("/Share", auth.GetShareToken)
diff --git a/pkg/glob/LICENSE b/pkg/glob/LICENSE
new file mode 100644
index 0000000..bdfbd95
--- /dev/null
+++ b/pkg/glob/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Ryan Uber
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/pkg/glob/glob.go b/pkg/glob/glob.go
new file mode 100644
index 0000000..e67db3b
--- /dev/null
+++ b/pkg/glob/glob.go
@@ -0,0 +1,56 @@
+package glob
+
+import "strings"
+
+// The character which is treated like a glob
+const GLOB = "*"
+
+// Glob will test a string pattern, potentially containing globs, against a
+// subject string. The result is a simple true/false, determining whether or
+// not the glob pattern matched the subject text.
+func Glob(pattern, subj string) bool {
+	// Empty pattern can only match empty subject
+	if pattern == "" {
+		return subj == pattern
+	}
+
+	// If the pattern _is_ a glob, it matches everything
+	if pattern == GLOB {
+		return true
+	}
+
+	parts := strings.Split(pattern, GLOB)
+
+	if len(parts) == 1 {
+		// No globs in pattern, so test for equality
+		return subj == pattern
+	}
+
+	leadingGlob := strings.HasPrefix(pattern, GLOB)
+	trailingGlob := strings.HasSuffix(pattern, GLOB)
+	end := len(parts) - 1
+
+	// Go over the leading parts and ensure they match.
+	for i := 0; i < end; i++ {
+		idx := strings.Index(subj, parts[i])
+
+		switch i {
+		case 0:
+			// Check the first section. Requires special handling.
+			if !leadingGlob && idx != 0 {
+				return false
+			}
+		default:
+			// Check that the middle parts match.
+			if idx < 0 {
+				return false
+			}
+		}
+
+		// Trim evaluated text from subj as we loop over the pattern.
+		subj = subj[idx+len(parts[i]):]
+	}
+
+	// Reached the last section. Requires special handling.
+	return trailingGlob || strings.HasSuffix(subj, parts[end])
+}
diff --git a/pkg/glob/glob_test.go b/pkg/glob/glob_test.go
new file mode 100644
index 0000000..fa4edee
--- /dev/null
+++ b/pkg/glob/glob_test.go
@@ -0,0 +1,105 @@
+package glob
+
+import (
+	"strings"
+	"testing"
+)
+
+func testGlobMatch(t *testing.T, pattern, subj string) {
+	if !Glob(pattern, subj) {
+		t.Fatalf("%s should match %s", pattern, subj)
+	}
+}
+
+func testGlobNoMatch(t *testing.T, pattern, subj string) {
+	if Glob(pattern, subj) {
+		t.Fatalf("%s should not match %s", pattern, subj)
+	}
+}
+
+func TestEmptyPattern(t *testing.T) {
+	testGlobMatch(t, "", "")
+	testGlobNoMatch(t, "", "test")
+}
+
+func TestEmptySubject(t *testing.T) {
+	for _, pattern := range []string{
+		"",
+		"*",
+		"**",
+		"***",
+		"****************",
+		strings.Repeat("*", 1000000),
+	} {
+		testGlobMatch(t, pattern, "")
+	}
+
+	for _, pattern := range []string{
+		// No globs/non-glob characters
+		"test",
+		"*test*",
+
+		// Trailing characters
+		"*x",
+		"*****************x",
+		strings.Repeat("*", 1000000) + "x",
+
+		// Leading characters
+		"x*",
+		"x*****************",
+		"x" + strings.Repeat("*", 1000000),
+
+		// Mixed leading/trailing characters
+		"x*x",
+		"x****************x",
+		"x" + strings.Repeat("*", 1000000) + "x",
+	} {
+		testGlobNoMatch(t, pattern, "")
+	}
+}
+
+func TestPatternWithoutGlobs(t *testing.T) {
+	testGlobMatch(t, "test", "test")
+}
+
+func TestGlob(t *testing.T) {
+	// Matches
+	for _, pattern := range []string{
+		"*test",           // Leading glob
+		"this*",           // Trailing glob
+		"this*test",       // Middle glob
+		"*is *",           // String in between two globs
+		"*is*a*",          // Lots of globs
+		"**test**",        // Double glob characters
+		"**is**a***test*", // Varying number of globs
+		"* *",             // White space between globs
+		"*",               // Lone glob
+		"**********",      // Nothing but globs
+		"*Ѿ*",             // Unicode with globs
+		"*is a ϗѾ *",      // Mixed ASCII/unicode
+	} {
+		testGlobMatch(t, pattern, "this is a ϗѾ test")
+	}
+
+	// Non-matches
+	for _, pattern := range []string{
+		"test*",               // Implicit substring match
+		"*is",                 // Partial match
+		"*no*",                // Globs without a match between them
+		" ",                   // Plain white space
+		"* ",                  // Trailing white space
+		" *",                  // Leading white space
+		"*ʤ*",                 // Non-matching unicode
+		"this*this is a test", // Repeated prefix
+	} {
+		testGlobNoMatch(t, pattern, "this is a test")
+	}
+}
+
+func BenchmarkGlob(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		if !Glob("*quick*fox*dog", "The quick brown fox jumped over the lazy dog") {
+			b.Fatalf("should match")
+		}
+	}
+}
diff --git a/pkg/tester/tester.go b/pkg/tester/tester.go
index f6f25ef..11f7daa 100644
--- a/pkg/tester/tester.go
+++ b/pkg/tester/tester.go
@@ -49,7 +49,7 @@ func DoRequestOnServer(t *testing.T, hostname string, port string, jar *cookieja
 	// or create your own transport, there's an example on godoc.
 	http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
 		addrAndPort := strings.Split(addr, ":")
-		if strings.HasSuffix(addrAndPort[0], "vestibule.io") {
+		if strings.HasSuffix(addrAndPort[0], "sdk-go.io") {
 			addr = "127.0.0.1:" + addrAndPort[1]
 		}
 		return dialer.DialContext(ctx, network, addr)
diff --git a/testdata/static/index.html b/testdata/static/index.html
deleted file mode 100644
index c30a977..0000000
--- a/testdata/static/index.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta http-equiv="X-UA-Compatible" content="ie=edge">
-    <title>Vestibule Test</title>
-</head>
-<script>
-
-    async function getData() {
-        data = await fetch("https://api.vestibule.127.0.0.1.nip.io:1443/", {
-            "credentials": "include",
-            "method": "GET",
-            "mode": "cors"
-        });
-        result = await data.json();
-        document.getElementById('api-result').innerHTML = JSON.stringify(result);
-    }
-
-</script>
-
-<body onload="getData()">
-    <h1>This is a test !</h1>
-    <h2>API fetch result :</h2>
-    <div id="api-result">NO RESULT (YET)</div>
-</body>
-
-</html>
\ No newline at end of file
diff --git a/web/index.html b/web/index.html
index f72817d..717b324 100644
--- a/web/index.html
+++ b/web/index.html
@@ -1,6 +1,6 @@
 <!DOCTYPE html>
 <html lang="en" class="has-navbar-fixed-top">
-  <title>Vestibule</title>
+  <title>SDK-GO</title>
   <meta charset="UTF-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1" />
   <link rel="icon" href="assets/brand/favicon.ico" />
-- 
GitLab