added api and gitignore update

trunk
Adam Veldhousen 3 years ago
parent 82c888fa38
commit d40367a25d
Signed by: adam
GPG Key ID: 6DB29003C6DD1E4B

3
.gitignore vendored

@ -1,2 +1,5 @@
# db
db/.data
# api
api/.bin

File diff suppressed because one or more lines are too long

@ -0,0 +1,11 @@
module git.vdhsn.com/adam/bodytrack
go 1.15
require (
github.com/go-chi/chi v1.5.4
github.com/go-chi/chi/v5 v5.0.2
github.com/go-chi/jwtauth/v5 v5.0.1
github.com/lib/pq v1.10.0
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
)

@ -0,0 +1,73 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 h1:sgNeV1VRMDzs6rzyPpxyM0jp317hnwiq58Filgag2xw=
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0/go.mod h1:J70FGZSbzsjecRTiTzER+3f1KZLNaXkuv+yeFTKoxM8=
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-chi/chi/v5 v5.0.2 h1:4xKeALZdMEsuI5s05PU2Bm89Uc5iM04qFubUCl5LfAQ=
github.com/go-chi/chi/v5 v5.0.2/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/jwtauth v1.2.0 h1:Z116SPpevIABBYsv8ih/AHYBHmd4EufKSKsLUnWdrTM=
github.com/go-chi/jwtauth/v5 v5.0.1 h1:eyJ6Yx5VphEfjkqpZ7+LJEWThzyIcF5aN2QVpgqSIu0=
github.com/go-chi/jwtauth/v5 v5.0.1/go.mod h1:+JtcRYGZsnA4+ur1LFlb4Bei3O9WeUzoMfDZWfUJuoY=
github.com/goccy/go-json v0.4.8 h1:TfwOxfSp8hXH+ivoOk36RyDNmXATUETRdaNWDaZglf8=
github.com/goccy/go-json v0.4.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/lestrrat-go/backoff/v2 v2.0.7 h1:i2SeK33aOFJlUNJZzf2IpXRBvqBBnaGXfY5Xaop/GsE=
github.com/lestrrat-go/backoff/v2 v2.0.7/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/codegen v1.0.0/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc=
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/jwx v1.1.6 h1:VfyUo2PAU4lO/liwhdwiSZ55/QZDLTT3EYY5z9KfwZs=
github.com/lestrrat-go/jwx v1.1.6/go.mod h1:c+R8G7qsaFNmTzYjU98A+sMh8Bo/MJqO9GnpqR+X024=
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/pdebug/v3 v3.0.1/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4=
github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E=
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -0,0 +1,109 @@
package handlers
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"git.vdhsn.com/adam/bodytrack/internal/services"
"github.com/go-chi/chi/v5"
)
type ApiHandler interface {
Subscribe(chi.Router) error
}
type UserHandler struct {
Conn services.DB
}
func (u UserHandler) Subscribe(c chi.Router) error {
c.Post("/user/auth", u.login)
// c.PUT("/user", u.register)
c.Group(func(r chi.Router) {
r.Use(services.JWTVerifier)
// r.Get("/user", u.getProfile)
// r.Post("/user", u.updateProfile)
// r.Post("/user/verify", u.verifyEmail)
})
return nil
}
func (u UserHandler) login(res http.ResponseWriter, req *http.Request) {
type loginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
var lr loginRequest
if err := ParseCall(&lr, res, req); err != nil {
return
}
fail := APIResp{Payload: "Email or password incorrect"}
if lr.Email == "" || lr.Password == "" {
fail.Write(res)
return
}
profile, err := u.Conn.GetProfileByEmail(lr.Email)
if err != nil {
log.Println(err)
fail.Write(res)
return
}
if services.PasswordEqual(profile.PasswordHash, lr.Password) {
token := services.GenerateJWT(profile)
res.Header().Set("Authorization", fmt.Sprintf("Bearer %s", token))
http.SetCookie(res, &http.Cookie{
Name: "user-auth",
Value: token,
Expires: time.Now().Add(time.Hour * 24 * 10),
})
APIResp{
Success: true,
Payload: struct {
Profile services.User `json:"profile"`
Token string `json:"token"`
}{Profile: profile, Token: token},
}.Write(res)
return
}
fail.Write(res)
}
type APIResp struct {
Success bool `json:"success"`
Payload interface{} `json:"payload"`
}
func (a APIResp) Write(res http.ResponseWriter) {
res.Header().Set("Content-Type", "application/json; utf-8")
enc := json.NewEncoder(res)
enc.SetIndent("\n", "\t")
enc.Encode(a)
}
func ParseCall(model interface{}, res http.ResponseWriter, req *http.Request) error {
defer req.Body.Close()
decoder := json.NewDecoder(req.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&model); err != nil {
a := APIResp{Success: false, Payload: err.Error()}
a.Write(res)
return err
}
return nil
}

@ -0,0 +1,36 @@
package services
import (
"encoding/base64"
"net/http"
"github.com/go-chi/jwtauth/v5"
"golang.org/x/crypto/bcrypt"
)
var tokenEncoder *jwtauth.JWTAuth
var JWTVerifier func(http.Handler) http.Handler
func GenerateHashFromPassword(plainTextPass string) (string, error) {
hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(plainTextPass), bcrypt.MinCost)
if err != nil {
return "", err
}
// Convert the hashed password to a base64 encoded string
return base64.URLEncoding.EncodeToString(hashedPasswordBytes), nil
}
func PasswordEqual(hashedPassword, plainTextPass string) bool {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainTextPass)) != nil
}
func InitJWT(secretTxt string) {
tokenEncoder = jwtauth.New("HS256", []byte(secretTxt), nil)
JWTVerifier = jwtauth.Verifier(tokenEncoder)
}
func GenerateJWT(u User) string {
_, tokenString, _ := tokenEncoder.Encode(map[string]interface{}{"id": u.ID})
return tokenString
}

@ -0,0 +1,56 @@
package services
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
var dbConn *sql.DB
type DB interface {
// VerifyEmail(email string) error
// RegisterUser(email, username, password string) (User, error)
// UpdateProfile(id int64, u User) error
// GetProfile(id int64) (User, error)
GetProfileByEmail(email string) (User, error)
}
func Connect(host, user, password string) (DB, error) {
psqlconn := fmt.Sprintf("host=%s port=5432 user=%s password=%s dbname=bodytrack sslmode=disable", host, user, password)
pgClient, err := sql.Open("postgres", psqlconn)
if err != nil {
return nil, err
}
if err := pgClient.Ping(); err != nil {
return nil, err
}
return pgdb{DB: pgClient}, nil
}
type pgdb struct{ *sql.DB }
func (db pgdb) GetProfileByEmail(email string) (User, error) {
sql := `SELECT
u.id, u.email, u.passwordhash, p.username, p.displayunit, p.birthdate, p.height
FROM Accounts.Users u JOIN Accounts.Profile p on u.id = p.userId
WHERE u.email = $1;
`
var u User
if err := db.QueryRow(sql, email).Scan(
&u.ID,
&u.Email,
&u.PasswordHash,
&u.Username,
&u.DisplayUnit,
&u.Birthdate,
&u.Height,
); err != nil {
return u, err
}
return u, nil
}

@ -0,0 +1,20 @@
package services
import "time"
type Unit string
const (
Metric = Unit("Metric")
Imperial = Unit("Imperial")
)
type User struct {
ID int64 `json:"id"`
Email string `json:"email"`
Username string `json:"username"`
DisplayUnit Unit `json:"displayUnit"`
Birthdate *time.Time `json:"birthdate"`
Height *float64 `json:"height"`
PasswordHash string `json:"-"`
}

@ -0,0 +1,41 @@
package main
import (
"log"
"net/http"
"time"
"git.vdhsn.com/adam/bodytrack/internal/handlers"
"git.vdhsn.com/adam/bodytrack/internal/services"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
services.InitJWT("ducks123")
r := chi.NewRouter()
r.Use(middleware.Logger)
db, err := services.Connect("localhost", "api", "api-user")
if err != nil {
log.Fatal(err)
}
handlers := []handlers.ApiHandler{
handlers.UserHandler{Conn: db},
}
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Timeout(60 * time.Second))
r.Route("/v1/api/", func(inner chi.Router) {
for _, h := range handlers {
if err := h.Subscribe(inner); err != nil {
log.Fatal(err)
}
}
})
log.Fatal(http.ListenAndServe(":3000", r))
}

@ -0,0 +1,15 @@
BIN := .bin
start: $(BIN)/api
./$(BIN)/api -db-host=localhost -jwt-secret=ducks123
clean:
@rm -rf $(BIN)
.PHONY: clean start
$(BIN)/api:
go build -v -o $@ .
$(BIN):
mkdir -p $@
Loading…
Cancel
Save