add user and password hash

This commit is contained in:
nochill 2023-03-13 09:24:59 +07:00
parent 85dd2a3134
commit d05b501360
23 changed files with 410 additions and 21 deletions

View File

@ -9,9 +9,8 @@ migratedown:
mock-generate:
mockgen -package mockdb -destination db/mock/store.go git.nochill.in/nochill/naice_pos/db/sqlc Store
sqlc:
sqlc generate
sqlc generate && make mock-generate
test:
go test -v -cover ./...

View File

@ -109,7 +109,7 @@ func TestGetProductApi(t *testing.T) {
func randomProduct() db.Product {
return db.Product{
ID: uuid.New(),
MerchantID: uuid.MustParse("d9b0e126-991e-46ac-8c61-5efdd605f75d"),
MerchantID: uuid.MustParse("a848090f-0409-4386-9caa-929ae6874dbb"),
Name: util.RandomString(5),
SellingPrice: util.RandomFloat(1000, 99999),
PurchasePrice: util.RandomFloat(999, 9999),

View File

@ -14,6 +14,7 @@ func NewServer(store db.Store) *Server {
server := &Server{store: store}
router := gin.Default()
router.POST("/merchants", server.createUser)
router.POST("/products", server.createProduct)
router.PATCH("/products", server.updateProduct)
router.GET("/product/:id", server.getProduct)

69
api/user.go Normal file
View File

@ -0,0 +1,69 @@
package api
import (
"database/sql"
"net/http"
db "git.nochill.in/nochill/naice_pos/db/sqlc"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/lib/pq"
)
type createUserRequest struct {
Email string `json:"email" binding:"required,email"`
Fullname string `json:"fullname" binding:"required"`
Password string `json:"password" binding:"required"`
OutletName string `json:"outlet_name" binding:"required"`
}
type createUserResponse struct {
ID uuid.UUID `json:"id"`
IndexID int64 `json:"index_id"`
Email string `json:"email"`
Fullname string `json:"fullname"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
Outlet db.Merchant `json:"outlet"`
}
func (server *Server) createUser(ctx *gin.Context) {
var req createUserRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}
arg := db.UserMerchantTxParams{
Email: req.Email,
Fullname: req.Fullname,
Password: req.Password,
OutletName: req.OutletName,
}
user, err := server.store.CreateUserMerchantTx(ctx, arg)
if err != nil {
if pqErr, ok := err.(*pq.Error); ok {
switch pqErr.Code.Name() {
case "foreign_key_violation", "unique_violation":
ctx.JSON(http.StatusConflict, errorResponse(err))
return
}
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
res := createUserResponse{
ID: user.OwnerProfile.ID,
IndexID: user.OwnerProfile.IndexID,
Email: user.OwnerProfile.Email,
Fullname: user.OwnerProfile.Fullname,
CreatedAt: sql.NullTime{Valid: true, Time: user.OutletProfile.CreatedAt.Time},
UpdatedAt: sql.NullTime{Valid: true, Time: user.OutletProfile.UpdatedAt.Time},
Outlet: user.OutletProfile,
}
ctx.JSON(http.StatusOK, res)
}

1
api/user_test.go Normal file
View File

@ -0,0 +1 @@
package api

View File

@ -3,7 +3,7 @@ CREATE TABLE users(
"index_id" bigserial not null,
"email" varchar unique not null,
"password" varchar not null,
"fullname" varchar,
"fullname" varchar not null,
"created_at" timestamp default(now()),
"updated_at" timestamp default(now())
);

View File

@ -51,6 +51,21 @@ func (mr *MockStoreMockRecorder) CreateCustomers(arg0, arg1 interface{}) *gomock
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCustomers", reflect.TypeOf((*MockStore)(nil).CreateCustomers), arg0, arg1)
}
// CreateMerchant mocks base method.
func (m *MockStore) CreateMerchant(arg0 context.Context, arg1 db.CreateMerchantParams) (db.Merchant, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateMerchant", arg0, arg1)
ret0, _ := ret[0].(db.Merchant)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateMerchant indicates an expected call of CreateMerchant.
func (mr *MockStoreMockRecorder) CreateMerchant(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMerchant", reflect.TypeOf((*MockStore)(nil).CreateMerchant), arg0, arg1)
}
// CreateProduct mocks base method.
func (m *MockStore) CreateProduct(arg0 context.Context, arg1 db.CreateProductParams) (db.Product, error) {
m.ctrl.T.Helper()
@ -111,6 +126,36 @@ func (mr *MockStoreMockRecorder) CreateSuppliers(arg0, arg1 interface{}) *gomock
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSuppliers", reflect.TypeOf((*MockStore)(nil).CreateSuppliers), arg0, arg1)
}
// CreateUser mocks base method.
func (m *MockStore) CreateUser(arg0 context.Context, arg1 db.CreateUserParams) (db.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateUser", arg0, arg1)
ret0, _ := ret[0].(db.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateUser indicates an expected call of CreateUser.
func (mr *MockStoreMockRecorder) CreateUser(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockStore)(nil).CreateUser), arg0, arg1)
}
// CreateUserMerchantTx mocks base method.
func (m *MockStore) CreateUserMerchantTx(arg0 context.Context, arg1 db.UserMerchantTxParams) (db.UserMerchantTxResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateUserMerchantTx", arg0, arg1)
ret0, _ := ret[0].(db.UserMerchantTxResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateUserMerchantTx indicates an expected call of CreateUserMerchantTx.
func (mr *MockStoreMockRecorder) CreateUserMerchantTx(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserMerchantTx", reflect.TypeOf((*MockStore)(nil).CreateUserMerchantTx), arg0, arg1)
}
// CustomersList mocks base method.
func (m *MockStore) CustomersList(arg0 context.Context, arg1 db.CustomersListParams) ([]db.Customer, error) {
m.ctrl.T.Helper()
@ -198,6 +243,21 @@ func (mr *MockStoreMockRecorder) GetMerchantByUserId(arg0, arg1 interface{}) *go
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMerchantByUserId", reflect.TypeOf((*MockStore)(nil).GetMerchantByUserId), arg0, arg1)
}
// GetPasswordByEmail mocks base method.
func (m *MockStore) GetPasswordByEmail(arg0 context.Context, arg1 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPasswordByEmail", arg0, arg1)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPasswordByEmail indicates an expected call of GetPasswordByEmail.
func (mr *MockStoreMockRecorder) GetPasswordByEmail(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPasswordByEmail", reflect.TypeOf((*MockStore)(nil).GetPasswordByEmail), arg0, arg1)
}
// GetProduct mocks base method.
func (m *MockStore) GetProduct(arg0 context.Context, arg1 uuid.UUID) (db.Product, error) {
m.ctrl.T.Helper()
@ -228,6 +288,21 @@ func (mr *MockStoreMockRecorder) GetStockForUpdateStock(arg0, arg1 interface{})
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStockForUpdateStock", reflect.TypeOf((*MockStore)(nil).GetStockForUpdateStock), arg0, arg1)
}
// GetUserById mocks base method.
func (m *MockStore) GetUserById(arg0 context.Context, arg1 uuid.UUID) (db.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserById", arg0, arg1)
ret0, _ := ret[0].(db.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserById indicates an expected call of GetUserById.
func (mr *MockStoreMockRecorder) GetUserById(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserById", reflect.TypeOf((*MockStore)(nil).GetUserById), arg0, arg1)
}
// ListProducts mocks base method.
func (m *MockStore) ListProducts(arg0 context.Context, arg1 db.ListProductsParams) ([]db.Product, error) {
m.ctrl.T.Helper()

View File

@ -1,3 +1,8 @@
-- name: CreateMerchant :one
INSERT INTO merchants (name,owner_id)
VALUES ($1, $2)
RETURNING *;
-- name: GetMerchantById :one
SELECT * FROM merchants
WHERE id = $1;

View File

@ -9,11 +9,9 @@ RETURNING *;
-- name: GetPasswordByEmail :one
SELECT password
FROM users
WHERE email = $1
RETURNING password;
WHERE email = $1;
-- name: GetUserById :one
SELECT *
FROM users
WHERE id = $1
RETURNING *;
WHERE id = $1;

View File

@ -1,7 +1,7 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.17.2
// source: merchant.sql
// source: merchants.sql
package db
@ -11,6 +11,31 @@ import (
"github.com/google/uuid"
)
const createMerchant = `-- name: CreateMerchant :one
INSERT INTO merchants (name,owner_id)
VALUES ($1, $2)
RETURNING id, index_id, name, owner_id, created_at, updated_at
`
type CreateMerchantParams struct {
Name string `json:"name"`
OwnerID uuid.UUID `json:"owner_id"`
}
func (q *Queries) CreateMerchant(ctx context.Context, arg CreateMerchantParams) (Merchant, error) {
row := q.db.QueryRowContext(ctx, createMerchant, arg.Name, arg.OwnerID)
var i Merchant
err := row.Scan(
&i.ID,
&i.IndexID,
&i.Name,
&i.OwnerID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getMerchantById = `-- name: GetMerchantById :one
SELECT id, index_id, name, owner_id, created_at, updated_at FROM merchants
WHERE id = $1

View File

@ -0,0 +1 @@
package db

View File

@ -114,7 +114,7 @@ type User struct {
IndexID int64 `json:"index_id"`
Email string `json:"email"`
Password string `json:"password"`
Fullname sql.NullString `json:"fullname"`
Fullname string `json:"fullname"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}

View File

@ -18,7 +18,7 @@ func createRandomProduct(t *testing.T) (Product, CreateProductParams) {
stock := util.RandomFloat(10, 10000)
arg := CreateProductParams{
MerchantID: uuid.MustParse("d9b0e126-991e-46ac-8c61-5efdd605f75d"),
MerchantID: uuid.MustParse("a848090f-0409-4386-9caa-929ae6874dbb"),
Name: util.RandomString(10),
SellingPrice: sellingPrice,
PurchasePrice: purchasePrice,

View File

@ -12,18 +12,22 @@ import (
type Querier interface {
CreateCustomers(ctx context.Context, arg CreateCustomersParams) (Customer, error)
CreateMerchant(ctx context.Context, arg CreateMerchantParams) (Merchant, error)
CreateProduct(ctx context.Context, arg CreateProductParams) (Product, error)
CreatePurchaseOrder(ctx context.Context, arg CreatePurchaseOrderParams) (PurchaseOrder, error)
CreatePurchaseOrderDetail(ctx context.Context, arg CreatePurchaseOrderDetailParams) (PurchaseOrderDetail, error)
CreateSuppliers(ctx context.Context, arg CreateSuppliersParams) (Supplier, error)
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
CustomersList(ctx context.Context, arg CustomersListParams) ([]Customer, error)
DeleteCustomer(ctx context.Context, id uuid.UUID) error
DeleteProduct(ctx context.Context, id uuid.UUID) error
DeleteSupplier(ctx context.Context, id uuid.UUID) error
GetMerchantById(ctx context.Context, id uuid.UUID) (Merchant, error)
GetMerchantByUserId(ctx context.Context, ownerID uuid.UUID) (Merchant, error)
GetPasswordByEmail(ctx context.Context, email string) (string, error)
GetProduct(ctx context.Context, id uuid.UUID) (Product, error)
GetStockForUpdateStock(ctx context.Context, id uuid.UUID) (Product, error)
GetUserById(ctx context.Context, id uuid.UUID) (User, error)
ListProducts(ctx context.Context, arg ListProductsParams) ([]Product, error)
SuppliersList(ctx context.Context, arg SuppliersListParams) ([]Supplier, error)
UpdateCustomer(ctx context.Context, id uuid.UUID) (Customer, error)

View File

@ -11,6 +11,7 @@ import (
type Store interface {
Querier
PurchaseOrderTx(ctx context.Context, arg PurchasoOrderTxParams) (PurchaseOrderTxResult, error)
CreateUserMerchantTx(ctx context.Context, arg UserMerchantTxParams) (UserMerchantTxResult, error)
}
type SQLStore struct {
@ -126,3 +127,46 @@ func (store *SQLStore) PurchaseOrderTx(ctx context.Context, arg PurchasoOrderTxP
return result, err
}
type UserMerchantTxParams struct {
Email string `json:"email"`
Fullname string `json:"fullname"`
Password string `json:"password"`
OutletName string `json:"outlet_name"`
}
type UserMerchantTxResult struct {
OwnerProfile User `json:"owner_profile"`
OutletProfile Merchant `json:"outlet_profile"`
}
func (store *SQLStore) CreateUserMerchantTx(ctx context.Context, arg UserMerchantTxParams) (UserMerchantTxResult, error) {
var result UserMerchantTxResult
err := store.execTx(ctx, func(q *Queries) error {
var err error
result.OwnerProfile, err = q.CreateUser(ctx, CreateUserParams{
Email: arg.Email,
Password: arg.Password,
Fullname: arg.Fullname,
})
if err != nil {
return err
}
result.OutletProfile, err = q.CreateMerchant(ctx, CreateMerchantParams{
Name: arg.OutletName,
OwnerID: result.OwnerProfile.ID,
})
if err != nil {
return err
}
return nil
})
return result, err
}

View File

@ -44,7 +44,9 @@ func TestPurchaseOrder(t *testing.T) {
products = append(products, purchaseProducts_1, purchaseProducts_2)
for i := 0; i < 5; i++ {
tries := 3
for i := 0; i < tries; i++ {
go func() {
result, err := store.PurchaseOrderTx(context.Background(), PurchasoOrderTxParams{
MerchantID: supplier.MerchantID,
@ -61,7 +63,7 @@ func TestPurchaseOrder(t *testing.T) {
}()
}
for i := 0; i < 5; i++ {
for i := 0; i < tries; i++ {
err := <-errs
require.NoError(t, err)
@ -75,3 +77,38 @@ func TestPurchaseOrder(t *testing.T) {
require.NotZero(t, purchaseOrder.PaidNominal, product1.PurchasePrice+product2.PurchasePrice)
}
}
func TestCreateUserMerchant(t *testing.T) {
store := NewStore(testDB)
errs := make(chan error)
results := make(chan UserMerchantTxResult)
tries := 3
for i := 0; i < tries; i++ {
randString := util.RandomString(10)
password, err := util.HashPassword(randString)
require.NoError(t, err)
go func() {
result, err := store.CreateUserMerchantTx(context.Background(), UserMerchantTxParams{
Email: util.RandomEmail(),
Fullname: util.RandomString(6),
Password: password,
OutletName: util.RandomString(6),
})
errs <- err
results <- result
}()
}
for i := 0; i < tries; i++ {
err := <-errs
require.NoError(t, err)
result := <-results
require.NotEmpty(t, result)
}
}

View File

@ -11,7 +11,7 @@ import (
func createRandomSupplier(t *testing.T) (Supplier, CreateSuppliersParams) {
arg := CreateSuppliersParams{
MerchantID: uuid.MustParse("d9b0e126-991e-46ac-8c61-5efdd605f75d"),
MerchantID: uuid.MustParse("a848090f-0409-4386-9caa-929ae6874dbb"),
Name: util.RandomString(10),
}

76
db/sqlc/users.sql.go Normal file
View File

@ -0,0 +1,76 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.17.2
// source: users.sql
package db
import (
"context"
"github.com/google/uuid"
)
const createUser = `-- name: CreateUser :one
INSERT INTO users (
email,
password,
fullname
) VALUES ($1, $2, $3)
RETURNING id, index_id, email, password, fullname, created_at, updated_at
`
type CreateUserParams struct {
Email string `json:"email"`
Password string `json:"password"`
Fullname string `json:"fullname"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, createUser, arg.Email, arg.Password, arg.Fullname)
var i User
err := row.Scan(
&i.ID,
&i.IndexID,
&i.Email,
&i.Password,
&i.Fullname,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getPasswordByEmail = `-- name: GetPasswordByEmail :one
SELECT password
FROM users
WHERE email = $1
`
func (q *Queries) GetPasswordByEmail(ctx context.Context, email string) (string, error) {
row := q.db.QueryRowContext(ctx, getPasswordByEmail, email)
var password string
err := row.Scan(&password)
return password, err
}
const getUserById = `-- name: GetUserById :one
SELECT id, index_id, email, password, fullname, created_at, updated_at
FROM users
WHERE id = $1
`
func (q *Queries) GetUserById(ctx context.Context, id uuid.UUID) (User, error) {
row := q.db.QueryRowContext(ctx, getUserById, id)
var i User
err := row.Scan(
&i.ID,
&i.IndexID,
&i.Email,
&i.Password,
&i.Fullname,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

7
db/sqlc/users_test.go Normal file
View File

@ -0,0 +1,7 @@
package db
import "testing"
func TestGetUserByID(t *testing.T) {
}

2
go.mod
View File

@ -11,6 +11,7 @@ require (
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.2
github.com/tabbed/pqtype v0.1.1
golang.org/x/crypto v0.7.0
)
require (
@ -42,7 +43,6 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.10 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect

19
util/password.go Normal file
View File

@ -0,0 +1,19 @@
package util
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func HashPassword(password string) (string, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("Failed to hash password: %w", err)
}
return string(hashedPassword), nil
}
func CheckPassword(password string, hashedPassword string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}

23
util/password_test.go Normal file
View File

@ -0,0 +1,23 @@
package util
import (
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
)
func TestPasswordUtil(t *testing.T) {
password := RandomString(10)
hashedPassword, err := HashPassword(password)
require.NoError(t, err)
require.NotEmpty(t, hashedPassword)
err = CheckPassword(password, hashedPassword)
require.NoError(t, err)
wrongPassword := RandomString(5)
err = CheckPassword(wrongPassword, hashedPassword)
require.EqualError(t, err, bcrypt.ErrMismatchedHashAndPassword.Error())
}

View File

@ -1,6 +1,7 @@
package util
import (
"fmt"
"math/rand"
"strings"
"time"
@ -38,3 +39,7 @@ func RandomString(n int) string {
return sb.String()
}
func RandomEmail() string {
return fmt.Sprintf("%s@mail.com", RandomString(5))
}