diff --git a/.env b/.env index a7356d5..e795ae2 100644 --- a/.env +++ b/.env @@ -1,5 +1,7 @@ +APP_ENV=development user=postgres password=postgres dbname=recipes_db +dbport=5432 host=database -port=5432 \ No newline at end of file +serverport=8080 \ No newline at end of file diff --git a/auth/auth.claims.go b/auth/auth.claims.go deleted file mode 100644 index 3d6de10..0000000 --- a/auth/auth.claims.go +++ /dev/null @@ -1,30 +0,0 @@ -package auth - -import ( - "fmt" - "net/http" - "strings" - - "github.com/golang-jwt/jwt" -) - -func GetClaimsFromToken(r *http.Request) (jwt.MapClaims, error) { - claims := jwt.MapClaims{} - var jwtKey = []byte("SecretYouShouldHide") - authHeader := strings.Split(r.Header.Get("Authorization"), "Bearer ") - if len(authHeader) != 2 { - return nil, fmt.Errorf("http.StatusUnauthorized") - // w.WriteHeader(http.StatusUnauthorized) - } else { - tokenString := authHeader[1] - _, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { - return jwtKey, nil - }) - - if err != nil { - return nil, err - } - - return claims, nil - } -} diff --git a/database/db.go b/database/db.go new file mode 100644 index 0000000..d7e0c99 --- /dev/null +++ b/database/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.19.1 + +package database + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/database/models.go b/database/models.go new file mode 100644 index 0000000..4c886e3 --- /dev/null +++ b/database/models.go @@ -0,0 +1,52 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.19.1 + +package database + +import ( + "database/sql" + "time" +) + +type Ingredient struct { + ID int32 + Name string + CreatedOn time.Time + UpdatedOn time.Time +} + +type IngredientQuantityType struct { + ID int32 + IngredientID sql.NullInt32 + QuantityTypeID sql.NullInt32 + Amount int32 + CreatedOn time.Time + UpdatedOn time.Time +} + +type QuantityType struct { + ID int32 + Type string + CreatedOn time.Time + UpdatedOn time.Time +} + +type Recipe struct { + ID int32 + RecipeUserID int32 + RecipeName string + RecipeSteps string + CreatedOn time.Time + UpdatedOn time.Time +} + +type RecipeUser struct { + ID int32 + FirstName string + LastName string + Email string + Password string + CreatedOn time.Time + UpdatedOn time.Time +} diff --git a/database/query.sql.go b/database/query.sql.go new file mode 100644 index 0000000..b22e861 --- /dev/null +++ b/database/query.sql.go @@ -0,0 +1,240 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.19.1 +// source: query.sql + +package database + +import ( + "context" +) + +const createRecipe = `-- name: CreateRecipe :one +INSERT INTO + recipe ( + recipe_user_id, + recipe_name, + recipe_steps, + created_on, + updated_on + ) +VALUES + ($1, $2, $3, now(), now()) RETURNING id, recipe_user_id, recipe_name, recipe_steps, created_on, updated_on +` + +type CreateRecipeParams struct { + RecipeUserID int32 + RecipeName string + RecipeSteps string +} + +func (q *Queries) CreateRecipe(ctx context.Context, arg CreateRecipeParams) (Recipe, error) { + row := q.db.QueryRowContext(ctx, createRecipe, arg.RecipeUserID, arg.RecipeName, arg.RecipeSteps) + var i Recipe + err := row.Scan( + &i.ID, + &i.RecipeUserID, + &i.RecipeName, + &i.RecipeSteps, + &i.CreatedOn, + &i.UpdatedOn, + ) + return i, err +} + +const createRecipeUser = `-- name: CreateRecipeUser :one +INSERT INTO + recipe_user ( + first_name, + last_name, + email, + password, + created_on, + updated_on + ) +VALUES + ($1, $2, $3, $4, now(), now()) RETURNING id, first_name, last_name, email, password, created_on, updated_on +` + +type CreateRecipeUserParams struct { + FirstName string + LastName string + Email string + Password string +} + +func (q *Queries) CreateRecipeUser(ctx context.Context, arg CreateRecipeUserParams) (RecipeUser, error) { + row := q.db.QueryRowContext(ctx, createRecipeUser, + arg.FirstName, + arg.LastName, + arg.Email, + arg.Password, + ) + var i RecipeUser + err := row.Scan( + &i.ID, + &i.FirstName, + &i.LastName, + &i.Email, + &i.Password, + &i.CreatedOn, + &i.UpdatedOn, + ) + return i, err +} + +const deleteRecipe = `-- name: DeleteRecipe :exec +DELETE FROM + recipe +WHERE + id = $1 + AND recipe_user_id = $2 +` + +type DeleteRecipeParams struct { + ID int32 + RecipeUserID int32 +} + +func (q *Queries) DeleteRecipe(ctx context.Context, arg DeleteRecipeParams) error { + _, err := q.db.ExecContext(ctx, deleteRecipe, arg.ID, arg.RecipeUserID) + return err +} + +const deleteRecipeUser = `-- name: DeleteRecipeUser :exec +DELETE FROM + recipe_user +WHERE + id = $1 +` + +func (q *Queries) DeleteRecipeUser(ctx context.Context, id int32) error { + _, err := q.db.ExecContext(ctx, deleteRecipeUser, id) + return err +} + +const getRecipe = `-- name: GetRecipe :one +SELECT + id, recipe_user_id, recipe_name, recipe_steps, created_on, updated_on +FROM + recipe +WHERE + id = $1 + AND recipe_user_id = $2 +LIMIT + 1 +` + +type GetRecipeParams struct { + ID int32 + RecipeUserID int32 +} + +func (q *Queries) GetRecipe(ctx context.Context, arg GetRecipeParams) (Recipe, error) { + row := q.db.QueryRowContext(ctx, getRecipe, arg.ID, arg.RecipeUserID) + var i Recipe + err := row.Scan( + &i.ID, + &i.RecipeUserID, + &i.RecipeName, + &i.RecipeSteps, + &i.CreatedOn, + &i.UpdatedOn, + ) + return i, err +} + +const getRecipeUserPwd = `-- name: GetRecipeUserPwd :one +SELECT + id, first_name, last_name, email, password, created_on, updated_on +FROM + recipe_user +WHERE + email = $1 +LIMIT + 1 +` + +func (q *Queries) GetRecipeUserPwd(ctx context.Context, email string) (RecipeUser, error) { + row := q.db.QueryRowContext(ctx, getRecipeUserPwd, email) + var i RecipeUser + err := row.Scan( + &i.ID, + &i.FirstName, + &i.LastName, + &i.Email, + &i.Password, + &i.CreatedOn, + &i.UpdatedOn, + ) + return i, err +} + +const listRecipes = `-- name: ListRecipes :many +SELECT + id, recipe_user_id, recipe_name, recipe_steps, created_on, updated_on +FROM + recipe +WHERE + recipe_user_id = $1 +ORDER BY + recipe_name +` + +func (q *Queries) ListRecipes(ctx context.Context, recipeUserID int32) ([]Recipe, error) { + rows, err := q.db.QueryContext(ctx, listRecipes, recipeUserID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Recipe + for rows.Next() { + var i Recipe + if err := rows.Scan( + &i.ID, + &i.RecipeUserID, + &i.RecipeName, + &i.RecipeSteps, + &i.CreatedOn, + &i.UpdatedOn, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateRecipe = `-- name: UpdateRecipe :exec +UPDATE + recipe + SET + recipe_name = $3, + recipe_steps = $4 + WHERE + id = $1 + AND recipe_user_id = $2 +` + +type UpdateRecipeParams struct { + ID int32 + RecipeUserID int32 + RecipeName string + RecipeSteps string +} + +func (q *Queries) UpdateRecipe(ctx context.Context, arg UpdateRecipeParams) error { + _, err := q.db.ExecContext(ctx, updateRecipe, + arg.ID, + arg.RecipeUserID, + arg.RecipeName, + arg.RecipeSteps, + ) + return err +} diff --git a/docker-compose.yml b/docker-compose.yml index a652792..c8301ac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,9 +15,14 @@ services: build: context: . dockerfile: Dockerfile - env_file: .env environment: - APP_ENV=development + - user=postgres + - password=postgres + - dbname=recipes_db + - dbport=5432 + - host=database + - serverport=8080 restart: always depends_on: - database diff --git a/go.mod b/go.mod index b916478..d8cb44a 100644 --- a/go.mod +++ b/go.mod @@ -2,15 +2,82 @@ module github.com/recipe-api go 1.19 -require github.com/lib/pq v1.10.7 +require github.com/lib/pq v1.10.9 require ( github.com/gorilla/mux v1.8.0 github.com/qustavo/dotsql v1.1.0 - golang.org/x/crypto v0.5.0 + golang.org/x/crypto v0.9.0 ) require ( + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/Masterminds/sprig/v3 v3.2.3 // indirect + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230321174746-8dcc6526cfb1 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/bgentry/speakeasy v0.1.0 // indirect + github.com/bytecodealliance/wasmtime-go/v8 v8.0.0 // indirect + github.com/cubicdaiya/gonp v1.0.4 // indirect + github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/denisenkom/go-mssqldb v0.9.0 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-logfmt/logfmt v0.5.0 // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/godror/godror v0.24.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/cel-go v0.16.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/huandu/xstrings v1.4.0 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.4.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect github.com/joho/godotenv v1.4.0 // indirect + github.com/kyleconroy/sqlc v1.19.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-oci8 v0.1.1 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/mitchellh/cli v1.1.5 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pganalyze/pg_query_go/v4 v4.2.1 // indirect + github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect + github.com/pingcap/log v0.0.0-20210906054005-afc726e70354 // indirect + github.com/pingcap/tidb/parser v0.0.0-20220725134311-c80026e61f00 // indirect + github.com/posener/complete v1.2.3 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/riza-io/grpc-go v0.2.0 // indirect + github.com/rubenv/sql-migrate v1.5.2 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.7.0 // indirect + go.uber.org/zap v1.19.1 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect + google.golang.org/grpc v1.56.2 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d7b2fa2..86dc233 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,282 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230321174746-8dcc6526cfb1 h1:X8MJ0fnN5FPdcGF5Ij2/OW+HgiJrRg3AfHAx1PJtIzM= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230321174746-8dcc6526cfb1/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bytecodealliance/wasmtime-go/v8 v8.0.0 h1:jP4sqm2PHgm3+eQ50zCoCdIyQFkIL/Rtkw6TT8OYPFI= +github.com/bytecodealliance/wasmtime-go/v8 v8.0.0/go.mod h1:tgazNLU7xSC2gfRAM8L4WyE+dgs5yp9FF5/tGebEQyM= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cubicdaiya/gonp v1.0.4 h1:ky2uIAJh81WiLcGKBVD5R7KsM/36W6IqqTy6Bo6rGws= +github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYYACpOI0I= +github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso= +github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +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.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk= +github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/godror/godror v0.24.2 h1:uxGAD7UdnNGjX5gf4NnEIGw0JAPTIFiqAyRBZTPKwXs= +github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +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.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/cel-go v0.16.0 h1:DG9YQ8nFCFXAs/FDDwBxmL1tpKNrdlGUM9U3537bX/Y= +github.com/google/cel-go v0.16.0/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.4.2 h1:u1gmGDwbdRUZiwisBm/Ky2M14uQyUP65bG8+20nnyrg= +github.com/jackc/pgx/v5 v5.4.2/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY= +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/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= +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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kyleconroy/sqlc v1.19.1 h1:ZO45HSdlGWSselFFAzl2jD5fqvEKT9hEeIFqErMK1Zo= +github.com/kyleconroy/sqlc v1.19.1/go.mod h1:A1OObCUZYaPKKHNc1eywbVySz4cA6Wq/lYVYht9asHE= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-oci8 v0.1.1 h1:aEUDxNAyDG0tv8CA3TArnDQNyc4EhnWlsfxRgDHABHM= +github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= +github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mxk/go-sqlite v0.0.0-20140611214908-167da9432e1f/go.mod h1:pkc41e3zYdLbnNZr/Zr5u/Ozr7D0p8EorhQiE+DmM4Y= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pganalyze/pg_query_go/v4 v4.2.1 h1:id/vuyIQccb9f6Yx3pzH5l4QYrxE3v6/m8RPlgMrprc= +github.com/pganalyze/pg_query_go/v4 v4.2.1/go.mod h1:aEkDNOXNM5j0YGzaAapwJ7LB3dLNj+bvbWcLv1hOVqA= +github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 h1:+FZIDR/D97YOPik4N4lPDaUcLDF/EQPogxtlHB2ZZRM= +github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg= +github.com/pingcap/log v0.0.0-20210906054005-afc726e70354 h1:SvWCbCPh1YeHd9yQLksvJYAgft6wLTY1aNG81tpyscQ= +github.com/pingcap/log v0.0.0-20210906054005-afc726e70354/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4= +github.com/pingcap/tidb/parser v0.0.0-20220725134311-c80026e61f00 h1:aDC/yAGx/jPEyrX+UPKV3GWg+4A4yG8ifuP6jBEhDi0= +github.com/pingcap/tidb/parser v0.0.0-20220725134311-c80026e61f00/go.mod h1:wjvp+T3/T9XYt0nKqGX3Kc1AKuyUcfno6LTc6b2A6ew= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/qustavo/dotsql v1.1.0 h1:Yw+x4HacArj41O4z4oDso1KZqQ+if7O2jj8igcLqGM0= github.com/qustavo/dotsql v1.1.0/go.mod h1:ypGu9g6a8LYpavOT8VBsJO+plC0tLW6onMxwMvyoZIM= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/riza-io/grpc-go v0.2.0 h1:2HxQKFVE7VuYstcJ8zqpN84VnAoJ4dCL6YFhJewNcHQ= +github.com/riza-io/grpc-go v0.2.0/go.mod h1:2bDvR9KkKC3KhtlSHfR3dAXjUMT86kg4UfWFyVGWqi8= +github.com/rubenv/sql-migrate v1.5.2 h1:bMDqOnrJVV/6JQgQ/MxOpU+AdO8uzYYA/TxFUBzFtS0= +github.com/rubenv/sql-migrate v1.5.2/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +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/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/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-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +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/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI= +google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers/handler.go b/handlers/handler.go new file mode 100644 index 0000000..41a40b1 --- /dev/null +++ b/handlers/handler.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" + "github.com/recipe-api/recipe" + "github.com/recipe-api/security" + "github.com/recipe-api/user" +) + +type Handler struct { + user *user.User + recipe *recipe.Recipe +} + +func NewHandler(user user.User, recipe recipe.Recipe) *Handler { + return &Handler{user: &user, recipe: &recipe} +} + +func (h *Handler) Start(muxRouter *mux.Router) { + muxRouter.HandleFunc("/register", h.registerHandler).Methods("POST") + muxRouter.HandleFunc("/login", h.loginHandler).Methods("POST") + + muxRouter.Handle("/recipe", security.VerifyToken(recipe.Get())).Methods("GET") + muxRouter.Handle("/recipe/{id}", security.VerifyToken(recipe.GetAll())).Methods("GET") + muxRouter.Handle("/recipe", security.VerifyToken(recipe.Insert())).Methods("POST") + muxRouter.Handle("/recipe/{id}", security.VerifyToken(recipe.Update())).Methods("PUT") + muxRouter.Handle("/recipe/{id}", security.VerifyToken(recipe.Delete())).Methods("DELETE") + + muxRouter.HandleFunc("/health-check", HealthCheck).Methods("GET") +} + +func HealthCheck(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "API is up and running") +} diff --git a/handlers/handlers.go b/handlers/handlers.go deleted file mode 100644 index 109433c..0000000 --- a/handlers/handlers.go +++ /dev/null @@ -1,38 +0,0 @@ -package handlers - -import ( - "fmt" - "log" - "net/http" - - "github.com/gorilla/mux" - "github.com/recipe-api/repository" -) - -func SetupRoutes(repo *repository.RecipeRepository) { - log.Println("Loading routes...") - r := mux.NewRouter() - - r.HandleFunc("/register", PostRegisterHandler(repo)).Methods("POST") - r.HandleFunc("/login", PostLoginHandler(repo)).Methods("POST") - - r.Handle("/recipe", Middleware(GetAllRecipesHandler(repo))).Methods("GET") - r.Handle("/recipe/{id}", Middleware(GetRecipeHandler(repo))).Methods("GET") - r.Handle("/recipe", Middleware(InsertRecipeHandler(repo))).Methods("POST") - r.Handle("/recipe/{id}", Middleware(UpdateRecipeHandler(repo))).Methods("PUT") - r.Handle("/recipe/{id}", Middleware(DeleteRecipeHandler(repo))).Methods("DELETE") - - r.HandleFunc("/health-check", HealthCheck).Methods("GET") - - http.Handle("api/", r) - err := http.ListenAndServe(":8080", r) - if err != nil { - log.Fatal(err) - } - log.Println("API is now up...") -} - -func HealthCheck(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, "API is up and running") -} diff --git a/handlers/handlers.middleware.go b/handlers/handlers.middleware.go deleted file mode 100644 index 3f0afcb..0000000 --- a/handlers/handlers.middleware.go +++ /dev/null @@ -1,27 +0,0 @@ -package handlers - -import ( - "context" - "log" - "net/http" - - "github.com/recipe-api/auth" -) - -var claimsKey string = "claims" - -func Middleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - claims, err := auth.GetClaimsFromToken(r) - - if err != nil { - log.Print(err) - w.WriteHeader(http.StatusUnauthorized) - } - - ctx := context.WithValue(r.Context(), claimsKey, claims) - // Access context values in handlers like this - // props, _ := r.Context().Value("props").(jwt.MapClaims) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} diff --git a/handlers/handlers.recipe_users.go b/handlers/handlers.recipe_users.go deleted file mode 100644 index 14ff13c..0000000 --- a/handlers/handlers.recipe_users.go +++ /dev/null @@ -1,108 +0,0 @@ -package handlers - -import ( - "encoding/json" - "log" - "net/http" - "strconv" - "time" - - "github.com/golang-jwt/jwt" - "github.com/recipe-api/models" - "github.com/recipe-api/repository" - "golang.org/x/crypto/bcrypt" -) - -func PostRegisterHandler(repo *repository.RecipeRepository) http.HandlerFunc { - fn := func(w http.ResponseWriter, r *http.Request) { - var register models.Register - if err := json.NewDecoder(r.Body).Decode(®ister); err != nil { - log.Print(err) - w.WriteHeader(http.StatusBadRequest) - return - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(register.Password), 8) - if err != nil { - log.Print(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - hashedPasswordStr := string(hashedPassword) - m, err := repo.InsertRecipeUser(register.Firstname, register.Lastname, register.Email, hashedPasswordStr) - - if err != nil { - log.Print(err) - w.WriteHeader(http.StatusUnprocessableEntity) - return - } - j, err := json.Marshal(&m) - if err != nil { - log.Print(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusCreated) - w.Write(j) - } - return http.HandlerFunc(fn) -} - -func PostLoginHandler(repo *repository.RecipeRepository) http.HandlerFunc { - fn := func(w http.ResponseWriter, r *http.Request) { - creds, shouldReturn := getCredentials(r, w) - if shouldReturn { - return - } - - ru, err := repo.GetRecipeUserPwd(creds.Email) - if err != nil { - log.Print(err) - w.WriteHeader(http.StatusNotFound) - return - } - - if err = bcrypt.CompareHashAndPassword([]byte(ru.Password), []byte(creds.Password)); err != nil { - w.WriteHeader(http.StatusUnauthorized) - } - - tokenString, err := generateJWT(ru.Id) - if err != nil { - log.Print(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write([]byte(tokenString)) - } - return http.HandlerFunc(fn) -} - -func getCredentials(r *http.Request, w http.ResponseWriter) (models.Credentials, bool) { - var creds models.Credentials - if err := json.NewDecoder(r.Body).Decode(&creds); err != nil { - log.Print(err) - w.WriteHeader(http.StatusBadRequest) - return models.Credentials{}, true - } - return creds, false -} - -func generateJWT(id int64) (string, error) { - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "exp": json.Number(strconv.FormatInt(time.Now().Add(time.Hour*time.Duration(1)).Unix(), 10)), - "iat": json.Number(strconv.FormatInt(time.Now().Unix(), 10)), - "recipe_user_id": id, - }) - - tokenString, err := token.SignedString([]byte("SecretYouShouldHide")) - - if err != nil { - return "", err - } - - return tokenString, nil -} diff --git a/handlers/handlers.recipes_test.go b/handlers/handlers.recipes_test.go deleted file mode 100644 index 4ceb814..0000000 --- a/handlers/handlers.recipes_test.go +++ /dev/null @@ -1,303 +0,0 @@ -package handlers - -import ( - "bytes" - "encoding/json" - "fmt" - "log" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gorilla/mux" - "github.com/recipe-api/models" - "github.com/recipe-api/recipeDb" - "github.com/recipe-api/repository" -) - -func TestGetRecipe(t *testing.T) { - SetupEnvVars() - recipeId := setupFixture() - - var recipe models.Recipe - db := recipeDb.NewRecipeDb() - repo := repository.NewRecipeRepository(db) - - req, err := http.NewRequest("GET", fmt.Sprintf("/recipe/%v", recipeId), nil) - - if err != nil { - t.Fatal(err) - } - - vars := map[string]string{ - "id": fmt.Sprint(recipeId), - } - - req = mux.SetURLVars(req, vars) - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(GetRecipeHandler(&repo)) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) - } - - json.NewDecoder(rr.Body).Decode(&recipe) - - if recipe.Id != recipeId { - t.Errorf("recipe id is wrong value: got %v want %v", - recipe.Id, 1) - } - - if recipe.RecipeName != "Nick's recipe" { - t.Errorf("recipe id is wrong value: got %v want %v", - recipe.RecipeName, "Nick's recipe") - } - - if recipe.RecipeSteps != "Some steps for Nick's recipe" { - t.Errorf("recipe id is wrong value: got %v want %v", - recipe.RecipeSteps, "Some steps for Nick's recipe") - } - - teardownFixture(recipeId) -} - -func TestInsertRecipe(t *testing.T) { - SetupEnvVars() - - recipeToInsert := models.Recipe{ - Id: 0, - RecipeName: "Nick's other recipe", - RecipeSteps: "Some other steps for Nick's recipe", - } - body, _ := json.Marshal(recipeToInsert) - db := recipeDb.NewRecipeDb() - repo := repository.NewRecipeRepository(db) - - req, err := http.NewRequest("POST", "/recipe", bytes.NewReader(body)) - if err != nil { - t.Fatal(err) - } - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(InsertRecipeHandler(&repo)) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusCreated { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) - } - - req, err = http.NewRequest("GET", "/recipe/1", nil) - if err != nil { - t.Fatal(err) - } - - var recipeId int64 - - json.NewDecoder(rr.Body).Decode(&recipeId) - - vars := map[string]string{ - "id": fmt.Sprint(recipeId), - } - - req = mux.SetURLVars(req, vars) - - rr = httptest.NewRecorder() - handler = http.HandlerFunc(GetRecipeHandler(&repo)) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) - } - - var recipe models.Recipe - json.NewDecoder(rr.Body).Decode(&recipe) - - if recipe.Id == 0 { - t.Errorf("recipe id is wrong value: got %v want %v", - 0, recipe.Id) - } - - if recipe.RecipeName != recipeToInsert.RecipeName { - t.Errorf("recipe id is wrong value: got %v want %v", - recipe.RecipeName, recipeToInsert.RecipeName) - } - - if recipe.RecipeSteps != recipeToInsert.RecipeSteps { - t.Errorf("recipe id is wrong value: got %v want %v", - recipe.RecipeSteps, recipeToInsert.RecipeSteps) - } - - teardownFixture(recipeId) -} - -func TestUpdateRecipe(t *testing.T) { - SetupEnvVars() - recipeId := setupFixture() - - var recipe models.Recipe - db := recipeDb.NewRecipeDb() - repo := repository.NewRecipeRepository(db) - - recipeToUpdate := models.Recipe{ - Id: recipeId, - RecipeName: "This is the new name", - RecipeSteps: "These are the new steps", - } - - body, _ := json.Marshal(recipeToUpdate) - - req, err := http.NewRequest("PUT", fmt.Sprintf("/recipe/%v", recipeId), bytes.NewReader(body)) - if err != nil { - t.Fatal(err) - } - - vars := map[string]string{ - "id": fmt.Sprint(recipeId), - } - - req = mux.SetURLVars(req, vars) - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(UpdateRecipeHandler(&repo)) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) - } - - req = mux.SetURLVars(req, vars) - - rr = httptest.NewRecorder() - handler = http.HandlerFunc(GetRecipeHandler(&repo)) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) - } - - json.NewDecoder(rr.Body).Decode(&recipe) - - if recipe.Id != recipeId { - t.Errorf("recipe id is wrong value: got %v want %v", - recipe.Id, 1) - } - - if recipe.RecipeName != recipeToUpdate.RecipeName { - t.Errorf("recipe id is wrong value: got %v want %v", - recipe.RecipeName, recipeToUpdate.RecipeName) - } - - if recipe.RecipeSteps != recipeToUpdate.RecipeSteps { - t.Errorf("recipe id is wrong value: got %v want %v", - recipe.RecipeSteps, recipeToUpdate.RecipeSteps) - } - - teardownFixture(recipeId) -} - -func TestDeleteRecipe(t *testing.T) { - SetupEnvVars() - recipeId := setupFixture() - - req, err := http.NewRequest("DELETE", fmt.Sprintf("/recipe/%v", recipeId), nil) - db := recipeDb.NewRecipeDb() - repo := repository.NewRecipeRepository(db) - if err != nil { - log.Panic(err) - } - - vars := map[string]string{ - "id": fmt.Sprint(recipeId), - } - - req = mux.SetURLVars(req, vars) - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(DeleteRecipeHandler(&repo)) - - handler.ServeHTTP(rr, req) - - if rr.Result().StatusCode != 200 { - fmt.Printf("error expected: %v but got %v", 200, rr.Result().StatusCode) - } - - req, err = http.NewRequest("GET", fmt.Sprintf("/recipe/%v", recipeId), nil) - if err != nil { - t.Fatal(err) - } - - req = mux.SetURLVars(req, vars) - - rr = httptest.NewRecorder() - handler = http.HandlerFunc(GetRecipeHandler(&repo)) - - handler.ServeHTTP(rr, req) - - if status := rr.Code; status != http.StatusNotFound { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) - } -} - -func setupFixture() int64 { - recipeToInsert := models.Recipe{ - Id: 0, - RecipeName: "Nick's recipe", - RecipeSteps: "Some steps for Nick's recipe", - } - body, _ := json.Marshal(recipeToInsert) - db := recipeDb.NewRecipeDb() - repo := repository.NewRecipeRepository(db) - - req, err := http.NewRequest("POST", "/recipe", bytes.NewReader(body)) - if err != nil { - log.Panic(err) - } - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(InsertRecipeHandler(&repo)) - - handler.ServeHTTP(rr, req) - - var id int64 - - json.NewDecoder(rr.Body).Decode(&id) - return id -} - -func teardownFixture(recipeId int64) { - req, err := http.NewRequest("DELETE", fmt.Sprintf("/recipe/%v", recipeId), nil) - db := recipeDb.NewRecipeDb() - repo := repository.NewRecipeRepository(db) - - if err != nil { - log.Panic(err) - } - - vars := map[string]string{ - "id": fmt.Sprint(recipeId), - } - - req = mux.SetURLVars(req, vars) - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(DeleteRecipeHandler(&repo)) - - handler.ServeHTTP(rr, req) - - if rr.Result().StatusCode != 200 { - fmt.Printf("error with teardown fixture expected: %v but got %v", 200, rr.Result().StatusCode) - } -} diff --git a/handlers/handlers.test_helper.go b/handlers/handlers.test_helper.go deleted file mode 100644 index f597eba..0000000 --- a/handlers/handlers.test_helper.go +++ /dev/null @@ -1,67 +0,0 @@ -package handlers - -import ( - "bytes" - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "os" - - "github.com/recipe-api/models" - "github.com/recipe-api/recipeDb" - "github.com/recipe-api/repository" -) - -const ( - firstname string = "Test" - lastname string = "User" - email string = "testuser@gmail.com" - password string = "password" -) - -func SetupEnvVars() { - os.Setenv("user", "postgres") - os.Setenv("password", "postgres") - os.Setenv("dbname", "recipes_db") - os.Setenv("host", "localhost") - os.Setenv("port", "5432") -} - -func GetToken() string { - db := recipeDb.NewRecipeDb() - repo := repository.NewRecipeRepository(db) - - body, _ := json.Marshal(models.Register{ - Firstname: firstname, - Lastname: lastname, - Email: email, - Password: password, - }) - req, _ := http.NewRequest("POST", "/register", bytes.NewReader(body)) - - rr := httptest.NewRecorder() - handler := http.HandlerFunc(PostRegisterHandler(&repo)) - - handler.ServeHTTP(rr, req) - - body, _ = json.Marshal(models.Credentials{ - Email: email, - Password: password, - }) - - req, _ = http.NewRequest("POST", "/login", bytes.NewReader(body)) - - rr = httptest.NewRecorder() - handler = http.HandlerFunc(PostLoginHandler(&repo)) - handler.ServeHTTP(rr, req) - - bodyBytes, _ := io.ReadAll(rr.Body) - return string(bodyBytes) -} - -func Teardown(recipeUserId int) { - db := recipeDb.NewRecipeDb() - repo := repository.NewRecipeRepository(db) - repo.DeleteRecipeUser(recipeUserId) -} diff --git a/handlers/handlers.recipes.go b/handlers/recipeHandler.go similarity index 100% rename from handlers/handlers.recipes.go rename to handlers/recipeHandler.go diff --git a/handlers/userHandler.go b/handlers/userHandler.go new file mode 100644 index 0000000..a49a5cb --- /dev/null +++ b/handlers/userHandler.go @@ -0,0 +1,80 @@ +package handlers + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/recipe-api/repository" +) + +func (h Handler) registerHandler(w http.ResponseWriter, r *http.Request) { + var register repository.Register + if err := json.NewDecoder(r.Body).Decode(®ister); err != nil { + log.Print(err) + w.WriteHeader(http.StatusBadRequest) + + } + + if register.Firstname == "" { + w.Write([]byte("firstname is a required field")) + w.WriteHeader(http.StatusBadRequest) + } + + if register.Lastname == "" { + w.Write([]byte("lastname is a required field")) + w.WriteHeader(http.StatusBadRequest) + } + + if register.Email == "" { + w.Write([]byte("email is a required field")) + w.WriteHeader(http.StatusBadRequest) + } + + if register.Password == "" { + w.Write([]byte("password is a required field")) + w.WriteHeader(http.StatusBadRequest) + } + + userId, err := h.user.Register(register.Firstname, register.Lastname, register.Email, register.Password) + if err != nil { + log.Print(err) + w.WriteHeader(http.StatusUnprocessableEntity) + } + + j, err := json.Marshal(&userId) + if err != nil { + log.Print(err) + w.WriteHeader(http.StatusInternalServerError) + } + + w.WriteHeader(http.StatusCreated) + w.Write(j) +} + +func (h Handler) loginHandler(w http.ResponseWriter, r *http.Request) { + creds, shouldReturn := getCredentials(r, w) + if shouldReturn { + return + } + + token, err := h.user.Login(creds.Email, creds.Password) + + if err != nil { + log.Print(err) + w.WriteHeader(http.StatusNotFound) + return + } + + w.Write([]byte(*token)) +} + +func getCredentials(r *http.Request, w http.ResponseWriter) (repository.Credentials, bool) { + var creds repository.Credentials + if err := json.NewDecoder(r.Body).Decode(&creds); err != nil { + log.Print(err) + w.WriteHeader(http.StatusBadRequest) + return repository.Credentials{}, true + } + return creds, false +} diff --git a/main.go b/main.go index 93885d5..1775f2b 100644 --- a/main.go +++ b/main.go @@ -1,15 +1,88 @@ package main import ( - "github.com/recipe-api/handlers" - "github.com/recipe-api/recipeDb" + "context" + "database/sql" + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/gorilla/mux" + "github.com/joho/godotenv" + "github.com/recipe-api/database" + "github.com/recipe-api/handler" + "github.com/recipe-api/recipe" "github.com/recipe-api/repository" + "github.com/recipe-api/user" + migrate "github.com/rubenv/sql-migrate" + + _ "github.com/lib/pq" +) + +const ( + driver = "postgres" + migrationsDir = "migrations" + seconds = 30 ) func main() { - recipeDb.Migrate() - db := recipeDb.NewRecipeDb() - repo := repository.NewRecipeRepository(db) + err := godotenv.Load() + if err != nil { + log.Panic(err) + } + + psqlconn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + os.Getenv("host"), os.Getenv("dbport"), os.Getenv("user"), os.Getenv("password"), os.Getenv("dbname")) + + db, err := sql.Open(driver, psqlconn) + + if err != nil { + log.Fatal(err) + } + defer db.Close() + + migrations := &migrate.FileMigrationSource{ + Dir: migrationsDir, + } + + number, err := migrate.Exec(db, driver, migrations, migrate.Up) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Applied %d migrations!\n", number) + + queries := database.New(db) + + ctx, cancel := context.WithTimeout(context.Background(), seconds*time.Second) + defer cancel() + + go func() { + select { + case <-time.After(seconds * time.Second): + fmt.Println("overslept") + case <-ctx.Done(): + fmt.Println(ctx.Err()) // prints "context deadline exceeded" + } + }() + + userRepository := repository.NewUserRepository(queries, &ctx) + recipeRepository := repository.NewRecipeRepository(queries, &ctx) + + user := user.NewUser(&userRepository) + recipe := recipe.NewRecipe(&recipeRepository) + + log.Println("Loading routes...") + mr := mux.NewRouter() + h := handler.NewHandler(*user, *recipe) + + h.Start(mr) - handlers.SetupRoutes(&repo) + http.Handle("api/", mr) + serverPort := fmt.Sprintf(":%s", os.Getenv("serverport")) + err = http.ListenAndServe(serverPort, mr) + if err != nil { + log.Fatal(err) + } } diff --git a/migrations/001_users.sql b/migrations/001_users.sql new file mode 100644 index 0000000..87d4dbd --- /dev/null +++ b/migrations/001_users.sql @@ -0,0 +1,14 @@ +-- +migrate Up +CREATE TABLE IF NOT EXISTS recipe_user ( + id INTEGER PRIMARY KEY generated always as identity, + first_name VARCHAR(45) NOT NULL, + last_name VARCHAR(45) NOT NULL, + email VARCHAR(255) NOT NULL, + password TEXT NOT NULL, + created_on TIMESTAMP NOT NULL, + updated_on TIMESTAMP NOT NULL, + UNIQUE(email) +); + +-- +migrate Down +DROP TABLE recipe_user; \ No newline at end of file diff --git a/recipeDb/init.sql b/migrations/002_recipes.sql similarity index 63% rename from recipeDb/init.sql rename to migrations/002_recipes.sql index ff0173b..bf7fade 100644 --- a/recipeDb/init.sql +++ b/migrations/002_recipes.sql @@ -1,26 +1,13 @@ --- name: create-recipe_user-table -CREATE TABLE IF NOT EXISTS recipe_user ( - id INTEGER PRIMARY KEY generated always as identity, - first_name VARCHAR(45) NOT NULL, - last_name VARCHAR(45) NOT NULL, - email VARCHAR(255) NOT NULL, - password TEXT NOT NULL, - created_on TIMESTAMP NOT NULL, - updated_on TIMESTAMP NOT NULL, - UNIQUE(email) -); - --- name: create-recipe-table +-- +migrate Up CREATE TABLE IF NOT EXISTS recipe ( id INTEGER PRIMARY KEY generated always as identity, - recipe_user_id integer REFERENCES recipe_user (id), + recipe_user_id integer REFERENCES recipe_user (id) NOT NULL, recipe_name VARCHAR(45) NOT NULL, recipe_steps VARCHAR NOT NULL, created_on TIMESTAMP NOT NULL, updated_on TIMESTAMP NOT NULL ); --- name: create-ingredient-table CREATE TABLE IF NOT EXISTS ingredient ( id INTEGER PRIMARY KEY generated always as identity, name VARCHAR(45) NOT NULL, @@ -28,7 +15,6 @@ CREATE TABLE IF NOT EXISTS ingredient ( updated_on TIMESTAMP NOT NULL ); --- name: create-quantity_type-table CREATE TABLE IF NOT EXISTS quantity_type ( id INTEGER PRIMARY KEY generated always as identity, type VARCHAR(45) NOT NULL, @@ -36,7 +22,6 @@ CREATE TABLE IF NOT EXISTS quantity_type ( updated_on TIMESTAMP NOT NULL ); --- name: create-ingredient_quantity_type-table CREATE TABLE IF NOT EXISTS ingredient_quantity_type ( id INTEGER PRIMARY KEY generated always as identity, ingredient_id INTEGER REFERENCES ingredient (id), @@ -44,4 +29,9 @@ CREATE TABLE IF NOT EXISTS ingredient_quantity_type ( amount INTEGER NOT NULL, created_on TIMESTAMP NOT NULL, updated_on TIMESTAMP NOT NULL -); \ No newline at end of file +); + +-- +migrate Down +DROP TABLE ingredient_quantity_type; +DROP TABLE quantity_type; +DROP TABLE ingredient; \ No newline at end of file diff --git a/models/models.go b/models/models.go deleted file mode 100644 index 3fd6b8a..0000000 --- a/models/models.go +++ /dev/null @@ -1,68 +0,0 @@ -package models - -import ( - "time" -) - -type Recipe struct { - Id int64 `json:"id"` - RecipeUserId int64 `json:"recipeUserId"` - RecipeName string `json:"recipeName"` - RecipeSteps string `json:"recipeSteps"` - CreatedOn time.Time `json:"createdOn"` - UpdatedOn time.Time `json:"updatedOn"` - // TODO: implement this later. - // IngredientQuantity []IngredientQuantityType `json:"ingredientQuantity"` -} - -type RecipeUser struct { - Id int64 `json:"id"` - Firstname string `json:"firstname"` - Lastname string `json:"lastname"` - Email string `json:"email"` - Password string `json:"password"` - CreatedOn time.Time `json:"createdOn"` - UpdatedOn time.Time `json:"updatedOn"` -} - -type SaveRecipe struct { - RecipeName string `json:"recipeName"` - RecipeSteps string `json:"recipeSteps"` -} - -type Credentials struct { - Email string `json:"email"` - Password string `json:"password"` -} - -type Register struct { - Firstname string `json:"firstname"` - Lastname string `json:"lastname"` - Email string `json:"email"` - Password string `json:"password"` -} - -type Ingredient struct { - Id int `json:"id"` - Name string `json:"name"` - CreatedOn time.Time `json:"createdOn"` - UpdatedOn time.Time `json:"updatedOn"` -} - -type QuantityType struct { - Id int `json:"id"` - Type string `json:"type"` - CreatedOn time.Time `json:"createdOn"` - UpdatedOn time.Time `json:"updatedOn"` -} - -type IngredientQuantityType struct { - Id int `json:"id"` - IngredientId int `json:"ingredientId"` - QuantityTypeId int `json:"quantityTypeId"` - Amount int `json:"quantity"` - Ingredient Ingredient `json:"ingredient"` - QuantityType QuantityType `json:"quantityType"` - CreatedOn time.Time `json:"createdOn"` - UpdatedOn time.Time `json:"updatedOn"` -} diff --git a/query.sql b/query.sql new file mode 100644 index 0000000..8945fe2 --- /dev/null +++ b/query.sql @@ -0,0 +1,78 @@ +-- name: GetRecipe :one +SELECT + * +FROM + recipe +WHERE + id = $1 + AND recipe_user_id = $2 +LIMIT + 1; + +-- name: ListRecipes :many +SELECT + * +FROM + recipe +WHERE + recipe_user_id = $1 +ORDER BY + recipe_name; + +-- name: CreateRecipe :one +INSERT INTO + recipe ( + recipe_user_id, + recipe_name, + recipe_steps, + created_on, + updated_on + ) +VALUES + ($1, $2, $3, now(), now()) RETURNING *; + +-- name: UpdateRecipe :exec +UPDATE + recipe + SET + recipe_name = $3, + recipe_steps = $4 + WHERE + id = $1 + AND recipe_user_id = $2; + +-- name: DeleteRecipe :exec +DELETE FROM + recipe +WHERE + id = $1 + AND recipe_user_id = $2; + +-- name: CreateRecipeUser :one +INSERT INTO + recipe_user ( + first_name, + last_name, + email, + password, + created_on, + updated_on + ) +VALUES + ($1, $2, $3, $4, now(), now()) RETURNING *; + +-- name: DeleteRecipeUser :exec +DELETE FROM + recipe_user +WHERE + id = $1; + +-- name: GetRecipeUserPwd :one +SELECT + * +FROM + recipe_user +WHERE + email = $1 +LIMIT + 1; \ No newline at end of file diff --git a/recipe/recipe.go b/recipe/recipe.go new file mode 100644 index 0000000..fd0092f --- /dev/null +++ b/recipe/recipe.go @@ -0,0 +1,99 @@ +package recipe + +import ( + "log" + "net/http" + "strconv" + + "github.com/golang-jwt/jwt" + "github.com/gorilla/mux" + "github.com/recipe-api/database" + "github.com/recipe-api/repository" +) + +type Recipe struct { + repo *repository.RecipeRepository +} + +func NewRecipe(repo *repository.RecipeRepository) *Recipe { + return &Recipe{ + repo: repo, + } +} + +func (r Recipe) Get(recipeId int, recipeUserId int) (*database.Recipe, error) { + recipe, err := r.repo.GetRecipe(recipeId, recipeUserId) + + if err != nil { + return nil, err + } + return recipe, err +} + +func (r Recipe) GetAll(recipeUserId int) (*[]database.Recipe, error) { + recipes, err := r.repo.GetRecipes(recipeUserId) + + if err != nil { + return nil, err + } + return &recipes, err +} + +func (r Recipe) Insert(recipeUserId int, saveRecipe repository.SaveRecipe) (int32, error) { + id, err := r.repo.InsertRecipe(recipeUserId, &saveRecipe) + + if err != nil { + return 0, err + } + return id, err +} + +func (r Recipe) Update(recipeUserId int, recipeId int, saveRecipe repository.SaveRecipe) (bool, error) { + _, err := r.repo.UpdateRecipe(recipeId, recipeUserId, &saveRecipe) + + if err != nil { + return false, err + } + return true, nil +} + +func (r Recipe) Delete() http.HandlerFunc { + fn := func(w http.ResponseWriter, req *http.Request) { + recipeUserId, shouldReturn := getRecipeUserId(req, w) + if shouldReturn { + w.WriteHeader(http.StatusBadRequest) + return + } + + recipeId, err := strconv.Atoi(mux.Vars(req)["id"]) + + if err != nil { + log.Print(err) + w.WriteHeader(http.StatusBadRequest) + } + + _, err = r.repo.DeleteRecipe(recipeId, recipeUserId) + + if err != nil { + log.Print(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + } + return http.HandlerFunc(fn) +} + +func getRecipeUserId(r *http.Request, w http.ResponseWriter) (int, bool) { + props, _ := r.Context().Value("claims").(jwt.MapClaims) + recipeUserIdFloat, ok := props["recipe_user_id"].(float64) + + if !ok { + w.WriteHeader(http.StatusBadRequest) + return 0, true + } + + recipeUserId := int(recipeUserIdFloat) + return recipeUserId, false +} diff --git a/recipe/recipe_test.go1 b/recipe/recipe_test.go1 new file mode 100644 index 0000000..ea3d175 --- /dev/null +++ b/recipe/recipe_test.go1 @@ -0,0 +1,313 @@ +package recipe + +import ( + "context" + "database/sql" + "fmt" + "log" + "os" + "testing" + "time" + + _ "github.com/lib/pq" + "github.com/recipe-api/database" + "github.com/recipe-api/repository" + "github.com/recipe-api/user" +) + +const ( + driver = "postgres" + seconds = 30 + firstname = "John" + lastname = "Doe" + email = "johndoe@test.com" + password = "password123!" + recipeName = "something yummy" + recipeSteps = "some yummy recipe steps" +) + +func TestGetRecipe(t *testing.T) { + os.Setenv("user", "postgres") + os.Setenv("password", "postgres") + os.Setenv("dbname", "recipes_db") + os.Setenv("host", "localhost") + os.Setenv("dbport", "5432") + + psqlconn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + os.Getenv("host"), os.Getenv("dbport"), os.Getenv("user"), os.Getenv("password"), os.Getenv("dbname")) + + db, err := sql.Open(driver, psqlconn) + + if err != nil { + log.Fatal(err) + } + // defer db.Close() + + queries := database.New(db) + + ctx, cancel := context.WithTimeout(context.Background(), seconds*time.Second) + defer cancel() + userRepo := repository.NewUserRepository(queries, &ctx) + recipeRepo := repository.NewRecipeRepository(queries, &ctx) + user := user.NewUser(&userRepo) + recipe := NewRecipe(&recipeRepo) + + userId, _ := user.Register(firstname, lastname, email, password) + recipeId, err := recipe.Insert(int(userId), + repository.SaveRecipe{RecipeName: recipeName, RecipeSteps: recipeSteps}) + + if err != nil { + t.Errorf("expected nil but is: %v", err) + } + + if recipeId == 0 { + t.Errorf("expected recipeId to be > than 0 but is: %d", recipeId) + } + + myRecipe, err := recipe.Get(int(recipeId), int(userId)) + + if err != nil { + t.Errorf("expected nil but is: %v", err) + } + + if myRecipe.RecipeName != recipeName { + t.Errorf("expected %s but is: %s", recipeName, myRecipe.RecipeSteps) + } +} + +// func TestInsertRecipe(t *testing.T) { +// SetupEnvVars() + +// recipeToInsert := models.Recipe{ +// Id: 0, +// RecipeName: "Nick's other recipe", +// RecipeSteps: "Some other steps for Nick's recipe", +// } +// body, _ := json.Marshal(recipeToInsert) +// db := recipeDb.NewRecipeDb() +// repo := repository.NewRecipeRepository(db) + +// req, err := http.NewRequest("POST", "/recipe", bytes.NewReader(body)) +// if err != nil { +// t.Fatal(err) +// } + +// rr := httptest.NewRecorder() +// handler := http.HandlerFunc(InsertRecipeHandler(&repo)) + +// handler.ServeHTTP(rr, req) + +// if status := rr.Code; status != http.StatusCreated { +// t.Errorf("handler returned wrong status code: got %v want %v", +// status, http.StatusOK) +// } + +// req, err = http.NewRequest("GET", "/recipe/1", nil) +// if err != nil { +// t.Fatal(err) +// } + +// var recipeId int64 + +// json.NewDecoder(rr.Body).Decode(&recipeId) + +// vars := map[string]string{ +// "id": fmt.Sprint(recipeId), +// } + +// req = mux.SetURLVars(req, vars) + +// rr = httptest.NewRecorder() +// handler = http.HandlerFunc(GetRecipeHandler(&repo)) + +// handler.ServeHTTP(rr, req) + +// if status := rr.Code; status != http.StatusOK { +// t.Errorf("handler returned wrong status code: got %v want %v", +// status, http.StatusOK) +// } + +// var recipe models.Recipe +// json.NewDecoder(rr.Body).Decode(&recipe) + +// if recipe.Id == 0 { +// t.Errorf("recipe id is wrong value: got %v want %v", +// 0, recipe.Id) +// } + +// if recipe.RecipeName != recipeToInsert.RecipeName { +// t.Errorf("recipe id is wrong value: got %v want %v", +// recipe.RecipeName, recipeToInsert.RecipeName) +// } + +// if recipe.RecipeSteps != recipeToInsert.RecipeSteps { +// t.Errorf("recipe id is wrong value: got %v want %v", +// recipe.RecipeSteps, recipeToInsert.RecipeSteps) +// } + +// teardownFixture(recipeId) +// } + +// func TestUpdateRecipe(t *testing.T) { +// SetupEnvVars() +// recipeId := setupFixture() + +// var recipe models.Recipe +// db := recipeDb.NewRecipeDb() +// repo := repository.NewRecipeRepository(db) + +// recipeToUpdate := models.Recipe{ +// Id: recipeId, +// RecipeName: "This is the new name", +// RecipeSteps: "These are the new steps", +// } + +// body, _ := json.Marshal(recipeToUpdate) + +// req, err := http.NewRequest("PUT", fmt.Sprintf("/recipe/%v", recipeId), bytes.NewReader(body)) +// if err != nil { +// t.Fatal(err) +// } + +// vars := map[string]string{ +// "id": fmt.Sprint(recipeId), +// } + +// req = mux.SetURLVars(req, vars) + +// rr := httptest.NewRecorder() +// handler := http.HandlerFunc(UpdateRecipeHandler(&repo)) + +// handler.ServeHTTP(rr, req) + +// if status := rr.Code; status != http.StatusOK { +// t.Errorf("handler returned wrong status code: got %v want %v", +// status, http.StatusOK) +// } + +// req = mux.SetURLVars(req, vars) + +// rr = httptest.NewRecorder() +// handler = http.HandlerFunc(GetRecipeHandler(&repo)) + +// handler.ServeHTTP(rr, req) + +// if status := rr.Code; status != http.StatusOK { +// t.Errorf("handler returned wrong status code: got %v want %v", +// status, http.StatusOK) +// } + +// json.NewDecoder(rr.Body).Decode(&recipe) + +// if recipe.Id != recipeId { +// t.Errorf("recipe id is wrong value: got %v want %v", +// recipe.Id, 1) +// } + +// if recipe.RecipeName != recipeToUpdate.RecipeName { +// t.Errorf("recipe id is wrong value: got %v want %v", +// recipe.RecipeName, recipeToUpdate.RecipeName) +// } + +// if recipe.RecipeSteps != recipeToUpdate.RecipeSteps { +// t.Errorf("recipe id is wrong value: got %v want %v", +// recipe.RecipeSteps, recipeToUpdate.RecipeSteps) +// } + +// teardownFixture(recipeId) +// } + +// func TestDeleteRecipe(t *testing.T) { +// SetupEnvVars() +// recipeId := setupFixture() + +// req, err := http.NewRequest("DELETE", fmt.Sprintf("/recipe/%v", recipeId), nil) +// db := recipeDb.NewRecipeDb() +// repo := repository.NewRecipeRepository(db) +// if err != nil { +// log.Panic(err) +// } + +// vars := map[string]string{ +// "id": fmt.Sprint(recipeId), +// } + +// req = mux.SetURLVars(req, vars) + +// rr := httptest.NewRecorder() +// handler := http.HandlerFunc(DeleteRecipeHandler(&repo)) + +// handler.ServeHTTP(rr, req) + +// if rr.Result().StatusCode != 200 { +// fmt.Printf("error expected: %v but got %v", 200, rr.Result().StatusCode) +// } + +// req, err = http.NewRequest("GET", fmt.Sprintf("/recipe/%v", recipeId), nil) +// if err != nil { +// t.Fatal(err) +// } + +// req = mux.SetURLVars(req, vars) + +// rr = httptest.NewRecorder() +// handler = http.HandlerFunc(GetRecipeHandler(&repo)) + +// handler.ServeHTTP(rr, req) + +// if status := rr.Code; status != http.StatusNotFound { +// t.Errorf("handler returned wrong status code: got %v want %v", +// status, http.StatusOK) +// } +// } + +// func setupFixture() int64 { +// recipeToInsert := models.Recipe{ +// Id: 0, +// RecipeName: "Nick's recipe", +// RecipeSteps: "Some steps for Nick's recipe", +// } +// body, _ := json.Marshal(recipeToInsert) +// db := recipeDb.NewRecipeDb() +// repo := repository.NewRecipeRepository(db) + +// req, err := http.NewRequest("POST", "/recipe", bytes.NewReader(body)) +// if err != nil { +// log.Panic(err) +// } + +// rr := httptest.NewRecorder() +// handler := http.HandlerFunc(InsertRecipeHandler(&repo)) + +// handler.ServeHTTP(rr, req) + +// var id int64 + +// json.NewDecoder(rr.Body).Decode(&id) +// return id +// } + +// func teardownFixture(recipeId int64) { +// req, err := http.NewRequest("DELETE", fmt.Sprintf("/recipe/%v", recipeId), nil) +// db := recipeDb.NewRecipeDb() +// repo := repository.NewRecipeRepository(db) + +// if err != nil { +// log.Panic(err) +// } + +// vars := map[string]string{ +// "id": fmt.Sprint(recipeId), +// } + +// req = mux.SetURLVars(req, vars) + +// rr := httptest.NewRecorder() +// handler := http.HandlerFunc(DeleteRecipeHandler(&repo)) + +// handler.ServeHTTP(rr, req) + +// if rr.Result().StatusCode != 200 { +// fmt.Printf("error with teardown fixture expected: %v but got %v", 200, rr.Result().StatusCode) +// } +// } diff --git a/recipeDb/database.go b/recipeDb/database.go deleted file mode 100644 index 6b1e62d..0000000 --- a/recipeDb/database.go +++ /dev/null @@ -1,26 +0,0 @@ -package recipeDb - -import ( - "database/sql" - "fmt" - "log" - "os" - - _ "github.com/lib/pq" -) - -type RecipeDb struct { - SqlDb *sql.DB -} - -func NewRecipeDb() *RecipeDb { - psqlconn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", - os.Getenv("host"), os.Getenv("port"), os.Getenv("user"), os.Getenv("password"), os.Getenv("dbname")) - - db, err := sql.Open("postgres", psqlconn) - - if err != nil { - log.Panic(err) - } - return &RecipeDb{SqlDb: db} -} diff --git a/recipeDb/migrations.go b/recipeDb/migrations.go deleted file mode 100644 index e0a4138..0000000 --- a/recipeDb/migrations.go +++ /dev/null @@ -1,48 +0,0 @@ -package recipeDb - -import ( - "fmt" - "log" - "os" - "path" - "runtime" - - "github.com/qustavo/dotsql" -) - -func Migrate() { - db := NewRecipeDb() - dotSql := getDirectory() - - fmt.Println(os.Getenv("APP_ENV")) - fmt.Println("Running migrations") - db.runScript(dotSql, "create-recipe_user-table") - db.runScript(dotSql, "create-recipe-table") - db.runScript(dotSql, "create-ingredient-table") - db.runScript(dotSql, "create-quantity_type-table") - db.runScript(dotSql, "create-ingredient_quantity_type-table") - - // close database - defer db.SqlDb.Close() -} - -func getDirectory() *dotsql.DotSql { - // get relative path with runtime.caller - _, b, _, _ := runtime.Caller(0) - relativePath := path.Join(path.Dir(b)) - - dot, err := dotsql.LoadFromFile(fmt.Sprintf("%s/init.sql", relativePath)) - - if err != nil { - log.Fatal(err) - } - - return dot -} - -func (db *RecipeDb) runScript(dot *dotsql.DotSql, name string) { - _, err := dot.Exec(db.SqlDb, name) - if err != nil { - log.Fatal(err) - } -} diff --git a/repository/recipeRepository.go b/repository/recipeRepository.go new file mode 100644 index 0000000..6021d44 --- /dev/null +++ b/repository/recipeRepository.go @@ -0,0 +1,94 @@ +package repository + +import ( + "context" + "log" + + "github.com/recipe-api/database" +) + +type RecipeRepository struct { + queries *database.Queries + context *context.Context +} + +type SaveRecipe struct { + RecipeName string `json:"recipeName"` + RecipeSteps string `json:"recipeSteps"` +} + +func NewRecipeRepository(queries *database.Queries, context *context.Context) RecipeRepository { + return RecipeRepository{ + queries: queries, + context: context, + } +} + +func (r *RecipeRepository) GetRecipes(recipeUserId int) ([]database.Recipe, error) { + + var validRecipeUserId = int32(recipeUserId) + + recipes, err := r.queries.ListRecipes(*r.context, validRecipeUserId) + if err != nil { + log.Print(err) + } + + return recipes, err +} + +func (r *RecipeRepository) GetRecipe(recipeId int, recipeUserId int) (*database.Recipe, error) { + + var validRecipeUserId = int32(recipeUserId) + + validRecipeUserId = int32(recipeUserId) + + recipe, err := r.queries.GetRecipe(*r.context, database.GetRecipeParams{ID: int32(recipeId), RecipeUserID: validRecipeUserId}) + return &recipe, err +} + +func (r *RecipeRepository) InsertRecipe(recipeUserId int, ir *SaveRecipe) (b int32, err error) { + + var validRecipeUserId = int32(recipeUserId) + + newRecipe, err := r.queries.CreateRecipe(*r.context, database.CreateRecipeParams{ + RecipeUserID: validRecipeUserId, + RecipeName: ir.RecipeName, + RecipeSteps: ir.RecipeSteps, + }) + + return newRecipe.ID, err +} + +func (r *RecipeRepository) UpdateRecipe(recipeid int, recipeUserId int, recipe *SaveRecipe) (bool, error) { + + var validRecipeUserId = int32(recipeUserId) + + validRecipeUserId = int32(recipeUserId) + + err := r.queries.UpdateRecipe(*r.context, database.UpdateRecipeParams{ + ID: int32(recipeid), + RecipeUserID: validRecipeUserId, + RecipeName: recipe.RecipeName, + RecipeSteps: recipe.RecipeSteps, + }) + + if err != nil { + log.Print(err) + } + + return true, err +} + +func (r *RecipeRepository) DeleteRecipe(recipeId int, recipeUserId int) (d bool, err error) { + var validRecipeUserId = int32(recipeUserId) + + validRecipeUserId = int32(recipeUserId) + + r.queries.DeleteRecipe(*r.context, database.DeleteRecipeParams{ID: int32(recipeId), RecipeUserID: validRecipeUserId}) + + if err != nil { + log.Print(err) + } + + return true, err +} diff --git a/repository/repository.recipes.go b/repository/repository.recipes.go deleted file mode 100644 index 55e513e..0000000 --- a/repository/repository.recipes.go +++ /dev/null @@ -1,180 +0,0 @@ -package repository - -import ( - "database/sql" - "fmt" - "log" - - "github.com/recipe-api/models" - "github.com/recipe-api/recipeDb" -) - -type RecipeRepository struct { - db *recipeDb.RecipeDb -} - -func NewRecipeRepository(db *recipeDb.RecipeDb) RecipeRepository { - return RecipeRepository{ - db: db, - } -} - -func (r *RecipeRepository) GetRecipes(recipeUserId int) (*[]models.Recipe, error) { - rows, err := r.db.SqlDb.Query("SELECT * FROM recipe where recipe_user_id=$1", recipeUserId) - if err != nil { - log.Print(err) - } - defer rows.Close() - - var recipes []models.Recipe - - for rows.Next() { - var r models.Recipe - err := rows.Scan( - &r.Id, - &r.RecipeUserId, - &r.RecipeName, - &r.RecipeSteps, - &r.CreatedOn, - &r.UpdatedOn) - if err != nil { - log.Print(err) - } - - recipes = append(recipes, r) - } - err = rows.Err() - if err != nil { - log.Print(err) - } - - return &recipes, nil -} - -func (r *RecipeRepository) GetRecipe(recipeId int, recipeUserid int) (*models.Recipe, error) { - - row := r.db.SqlDb.QueryRow("SELECT * FROM recipe WHERE id=$1 AND recipe_user_id=$2", recipeId, recipeUserid) - var recipe models.Recipe - - switch err := row.Scan( - &recipe.Id, - &recipe.RecipeUserId, - &recipe.RecipeName, - &recipe.RecipeSteps, - &recipe.CreatedOn, - &recipe.UpdatedOn, - ); err { - case sql.ErrNoRows: - return nil, err - case nil: - return &recipe, nil - default: - panic(err) - } -} - -func (r *RecipeRepository) InsertRecipe(recipeUserId int, ir *models.SaveRecipe) (b int64, err error) { - var id int64 - var cols = "(recipe_user_id, recipe_name, recipe_steps, created_on, updated_on)" - var values = "($1, $2, $3, now(), now())" - - var query = fmt.Sprintf( - "INSERT INTO recipe %s VALUES %s RETURNING id", - cols, values, - ) - - if err := r.db.SqlDb.QueryRow( - query, - recipeUserId, ir.RecipeName, ir.RecipeSteps, - ).Scan(&id); err != nil { - panic(err) - } - - if err != nil { - log.Print(err) - return 0, err - } - - return id, nil -} - -func (r *RecipeRepository) UpdateRecipe(recipeid int, recipeUserId int, recipe *models.SaveRecipe) (d bool, err error) { - q := ` - UPDATE recipe - SET recipe_name = $3, recipe_steps = $4 - WHERE id = $1 AND recipe_user_id = $2;` - - _, err = r.db.SqlDb.Exec(q, recipeid, recipeUserId, recipe.RecipeName, recipe.RecipeSteps) - if err != nil { - log.Print(err) - } - - return true, nil -} - -func (r *RecipeRepository) DeleteRecipe(recipeId int, recipeUserId int) (d bool, err error) { - q := `DELETE FROM recipe WHERE id=$1 AND recipe_user_id=$2;` - _, err = r.db.SqlDb.Exec(q, recipeId, recipeUserId) - - if err != nil { - log.Print(err) - } - - return true, nil -} - -func (r *RecipeRepository) InsertRecipeUser(firstname string, lastname string, email string, hashedPwd string) (b int64, err error) { - var id int64 - var cols = "(first_name, last_name, email, password, created_on, updated_on)" - var values = "($1, $2, $3, $4, now(), now())" - - var query = fmt.Sprintf( - "INSERT INTO recipe_user %s VALUES %s RETURNING id", - cols, values, - ) - - if err := r.db.SqlDb.QueryRow( - query, - firstname, lastname, email, hashedPwd, - ).Scan(&id); err != nil { - log.Print(err) - return 0, err - } - - return id, nil -} - -func (r *RecipeRepository) DeleteRecipeUser(recipeUserId int) (d bool, err error) { - q := "DELETE FROM recipe_user WHERE id=$1;" - _, err = r.db.SqlDb.Exec(q, recipeUserId) - - if err != nil { - log.Print(err) - } - - return true, nil -} - -func (r *RecipeRepository) GetRecipeUserPwd(email string) (*models.RecipeUser, error) { - - row := r.db.SqlDb.QueryRow("SELECT * FROM recipe_user WHERE email=$1", email) - - var ru models.RecipeUser - - switch err := row.Scan( - &ru.Id, - &ru.Firstname, - &ru.Password, - &ru.Email, - &ru.Password, - &ru.CreatedOn, - &ru.UpdatedOn, - ); err { - case sql.ErrNoRows: - return nil, nil - case nil: - return &ru, nil - default: - panic(err) - } -} diff --git a/repository/userRepository.go b/repository/userRepository.go new file mode 100644 index 0000000..4169dfa --- /dev/null +++ b/repository/userRepository.go @@ -0,0 +1,71 @@ +package repository + +import ( + "context" + "log" + + "github.com/recipe-api/database" +) + +type UserRepository struct { + queries *database.Queries + context *context.Context +} + +type Credentials struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type Register struct { + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` + Email string `json:"email"` + Password string `json:"password"` +} + +func NewUserRepository(queries *database.Queries, context *context.Context) UserRepository { + return UserRepository{ + queries: queries, + context: context, + } +} + +func (ur *UserRepository) InsertRecipeUser(firstname string, lastname string, email string, hashedPwd string) (int64, error) { + user, err := ur.queries.CreateRecipeUser(*ur.context, database.CreateRecipeUserParams{ + FirstName: firstname, + LastName: lastname, + Email: email, + Password: hashedPwd, + }) + + if err != nil { + log.Print(err) + return 0, err + } + + return int64(user.ID), err +} + +func (ur *UserRepository) DeleteRecipeUser(recipeUserId int) (bool, error) { + + err := ur.queries.DeleteRecipeUser(*ur.context, int32(recipeUserId)) + + if err != nil { + log.Print(err) + return false, err + } + return true, err +} + +func (r *UserRepository) GetRecipeUserPwd(email string) (*database.RecipeUser, error) { + + user, err := r.queries.GetRecipeUserPwd(*r.context, email) + + if err != nil { + log.Print(err) + return nil, err + } + + return &user, err +} diff --git a/security/middleware.go b/security/middleware.go new file mode 100644 index 0000000..4b0e8aa --- /dev/null +++ b/security/middleware.go @@ -0,0 +1,69 @@ +package security + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/golang-jwt/jwt" +) + +var claimsKey string = "claims" + +func VerifyToken(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + claims, err := GetClaimsFromToken(r) + + if err != nil { + log.Print(err) + w.WriteHeader(http.StatusUnauthorized) + } + + ctx := context.WithValue(r.Context(), claimsKey, claims) + // Access context values in handlers like this + // props, _ := r.Context().Value("props").(jwt.MapClaims) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func GenerateToken(id int64) (string, error) { + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "exp": json.Number(strconv.FormatInt(time.Now().Add(time.Hour*time.Duration(1)).Unix(), 10)), + "iat": json.Number(strconv.FormatInt(time.Now().Unix(), 10)), + "recipe_user_id": id, + }) + + tokenString, err := token.SignedString([]byte("SecretYouShouldHide")) + + if err != nil { + return "", err + } + + return tokenString, nil +} + +func GetClaimsFromToken(r *http.Request) (jwt.MapClaims, error) { + claims := jwt.MapClaims{} + var jwtKey = []byte("SecretYouShouldHide") + authHeader := strings.Split(r.Header.Get("Authorization"), "Bearer ") + if len(authHeader) != 2 { + return nil, fmt.Errorf("http.StatusUnauthorized") + } else { + tokenString := authHeader[1] + _, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + return jwtKey, nil + }) + + if err != nil { + return nil, err + } + + return claims, nil + } +} diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..8f55243 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "postgresql" + queries: "query.sql" + schema: "migrations/" + gen: + go: + package: "database" + out: "database" \ No newline at end of file diff --git a/testSetup/testSetup.go b/testSetup/testSetup.go new file mode 100644 index 0000000..b7c50ac --- /dev/null +++ b/testSetup/testSetup.go @@ -0,0 +1,67 @@ +package testSetup + +import ( + "context" + "database/sql" + "fmt" + "os" + "time" + + "github.com/recipe-api/database" + "github.com/recipe-api/repository" + "github.com/recipe-api/security" +) + +const ( + firstname string = "Test" + lastname string = "User" + email string = "testuser@gmail.com" + password string = "password" + driver = "postgres" + migrationsDir = "migrations" + seconds = 30 +) + +// func SetupEnvVars() { +// os.Setenv("user", "postgres") +// os.Setenv("password", "postgres") +// os.Setenv("dbname", "recipes_db") +// os.Setenv("host", "localhost") +// os.Setenv("dbport", "5432") +// } + +func GetTestToken() string { + psqlconn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + os.Getenv("host"), os.Getenv("dbport"), os.Getenv("user"), os.Getenv("password"), os.Getenv("dbname")) + + db, _ := sql.Open(driver, psqlconn) + defer db.Close() + + queries := database.New(db) + + ctx, cancel := context.WithTimeout(context.Background(), seconds*time.Second) + defer cancel() + + repo := repository.NewUserRepository(queries, &ctx) + + userId, _ := repo.InsertRecipeUser(firstname, lastname, email, password) + + token, _ := security.GenerateToken(userId) + return token +} + +func Teardown(recipeUserId int) { + psqlconn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + os.Getenv("host"), os.Getenv("dbport"), os.Getenv("user"), os.Getenv("password"), os.Getenv("dbname")) + + db, _ := sql.Open(driver, psqlconn) + defer db.Close() + + queries := database.New(db) + + ctx, cancel := context.WithTimeout(context.Background(), seconds*time.Second) + defer cancel() + + repo := repository.NewUserRepository(queries, &ctx) + repo.DeleteRecipeUser(recipeUserId) +} diff --git a/handlers/handlers.recipe_users_test.go b/user/handlers.recipe_users_test.go1 similarity index 100% rename from handlers/handlers.recipe_users_test.go rename to user/handlers.recipe_users_test.go1 diff --git a/user/user.go b/user/user.go new file mode 100644 index 0000000..0609236 --- /dev/null +++ b/user/user.go @@ -0,0 +1,49 @@ +package user + +import ( + "github.com/recipe-api/repository" + "github.com/recipe-api/security" + "golang.org/x/crypto/bcrypt" +) + +type User struct { + repo *repository.UserRepository +} + +func NewUser(repo *repository.UserRepository) *User { + return &User{ + repo: repo, + } +} + +func (u User) Register(firstname string, lastname string, email string, password string) (int64, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 8) + if err != nil { + return 0, err + } + hashedPasswordStr := string(hashedPassword) + + userId, err := u.repo.InsertRecipeUser(firstname, lastname, email, hashedPasswordStr) + if err != nil { + return 0, err + } + return userId, nil +} + +func (u User) Login(email string, password string) (*string, error) { + user, err := u.repo.GetRecipeUserPwd(email) + if err != nil { + return nil, err + } + + if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + return nil, err + } + + tokenString, err := security.GenerateToken(int64(user.ID)) + if err != nil { + return nil, err + } + + return &tokenString, nil +}