add user authentication
This commit is contained in:
parent
93f570f332
commit
4738c8c590
@ -3,11 +3,27 @@ package api
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
db "git.nochill.in/nochill/naice_pos/db/sqlc"
|
||||
"git.nochill.in/nochill/naice_pos/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestServer(t *testing.T, store db.Store) *Server {
|
||||
config := util.Config{
|
||||
TokenSymmetricKey: util.RandomString(32),
|
||||
TokenDuration: time.Minute,
|
||||
}
|
||||
|
||||
server, err := NewServer(config, store)
|
||||
require.NoError(t, err)
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
|
@ -91,7 +91,7 @@ func TestGetProductApi(t *testing.T) {
|
||||
store := mockdb.NewMockStore(ctrl)
|
||||
tc.buildStubs(store)
|
||||
|
||||
server := NewServer(store)
|
||||
server := newTestServer(t, store)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
url := fmt.Sprintf("/product/%s", tc.productID)
|
||||
|
@ -1,28 +1,52 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
db "git.nochill.in/nochill/naice_pos/db/sqlc"
|
||||
"git.nochill.in/nochill/naice_pos/token"
|
||||
"git.nochill.in/nochill/naice_pos/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
config util.Config
|
||||
store db.Store
|
||||
tokenMaker token.Maker
|
||||
router *gin.Engine
|
||||
}
|
||||
|
||||
func NewServer(store db.Store) *Server {
|
||||
server := &Server{store: store}
|
||||
func NewServer(config util.Config, store db.Store) (*Server, error) {
|
||||
tokenMaker, err := token.NewPasetoMaker(config.TokenSymmetricKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create token maker: %w", err)
|
||||
}
|
||||
server := &Server{
|
||||
config: config,
|
||||
store: store,
|
||||
tokenMaker: tokenMaker,
|
||||
}
|
||||
|
||||
server.getRoutes()
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func (server *Server) getRoutes() {
|
||||
router := gin.Default()
|
||||
|
||||
router.POST("/user/login", server.loginUser)
|
||||
|
||||
router.POST("/user/merchants", server.createUserMerchant)
|
||||
|
||||
router.POST("/products", server.createProduct)
|
||||
router.PATCH("/products", server.updateProduct)
|
||||
router.GET("/product/:id", server.getProduct)
|
||||
|
||||
router.POST("/suppliers", server.createSupplier)
|
||||
|
||||
router.POST("/purchase-products", server.createPurchase)
|
||||
|
||||
server.router = router
|
||||
return server
|
||||
}
|
||||
|
||||
func (server *Server) Start(address string) error {
|
||||
|
83
api/user.go
83
api/user.go
@ -13,12 +13,12 @@ import (
|
||||
|
||||
type createUserMerchantRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Fullname string `json:"fullname" binding:"required"`
|
||||
Fullname string `json:"fullname" binding:"required,alphanum"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
OutletName string `json:"outlet_name" binding:"required"`
|
||||
OutletName string `json:"outlet_name" binding:"required,alphanum"`
|
||||
}
|
||||
|
||||
type createUserMerchantResponse struct {
|
||||
type userMerchantResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
IndexID int64 `json:"index_id"`
|
||||
Email string `json:"email"`
|
||||
@ -28,6 +28,18 @@ type createUserMerchantResponse struct {
|
||||
Outlet db.Merchant `json:"outlet"`
|
||||
}
|
||||
|
||||
func newUserMerchantResponse(user db.User, merchant db.Merchant) userMerchantResponse {
|
||||
return userMerchantResponse{
|
||||
ID: user.ID,
|
||||
IndexID: user.IndexID,
|
||||
Email: user.Email,
|
||||
Fullname: user.Fullname,
|
||||
CreatedAt: sql.NullTime{Valid: true, Time: user.CreatedAt.Time},
|
||||
UpdatedAt: sql.NullTime{Valid: true, Time: user.UpdatedAt.Time},
|
||||
Outlet: merchant,
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) createUserMerchant(ctx *gin.Context) {
|
||||
var req createUserMerchantRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
@ -61,15 +73,74 @@ func (server *Server) createUserMerchant(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res := createUserMerchantResponse{
|
||||
res := userMerchantResponse{
|
||||
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},
|
||||
CreatedAt: sql.NullTime{Valid: true, Time: user.OwnerProfile.CreatedAt.Time},
|
||||
UpdatedAt: sql.NullTime{Valid: true, Time: user.OwnerProfile.UpdatedAt.Time},
|
||||
Outlet: user.OutletProfile,
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
type userLoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type userLoginResponse struct {
|
||||
AccesToken string `json:"access_token"`
|
||||
UserMerchantResponse userMerchantResponse
|
||||
}
|
||||
|
||||
func (server *Server) loginUser(ctx *gin.Context) {
|
||||
var req userLoginRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, errorResponse(err))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := server.store.GetUserByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
ctx.JSON(http.StatusNotFound, errorResponse(err))
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
|
||||
return
|
||||
}
|
||||
|
||||
outlet, err := server.store.GetMerchantByUserId(ctx, user.ID)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = util.CheckPassword(req.Password, user.Password)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
|
||||
return
|
||||
}
|
||||
|
||||
accessToken, err := server.tokenMaker.CreateToken(
|
||||
user.Email,
|
||||
server.config.TokenDuration,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
|
||||
return
|
||||
}
|
||||
|
||||
userMerchant := newUserMerchantResponse(user, outlet)
|
||||
|
||||
res := userLoginResponse{
|
||||
AccesToken: accessToken,
|
||||
UserMerchantResponse: userMerchant,
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, res)
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ func TestCreateUserMerchantAPI(t *testing.T) {
|
||||
store := mockdb.NewMockStore(ctrl)
|
||||
tc.buildStubs(store)
|
||||
|
||||
server := NewServer(store)
|
||||
server := newTestServer(t, store)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
data, err := json.Marshal(tc.body)
|
||||
@ -124,7 +124,7 @@ func RandomUser(t *testing.T) (userMerchant db.UserMerchantTxParams, password st
|
||||
return
|
||||
}
|
||||
|
||||
func requireBodyMatchUserMerchant(t *testing.T, body *bytes.Buffer, userMerchant createUserMerchantResponse) {
|
||||
func requireBodyMatchUserMerchant(t *testing.T, body *bytes.Buffer, userMerchant userMerchantResponse) {
|
||||
data, err := ioutil.ReadAll(body)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -288,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)
|
||||
}
|
||||
|
||||
// GetUserByEmail mocks base method.
|
||||
func (m *MockStore) GetUserByEmail(arg0 context.Context, arg1 string) (db.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUserByEmail", arg0, arg1)
|
||||
ret0, _ := ret[0].(db.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetUserByEmail indicates an expected call of GetUserByEmail.
|
||||
func (mr *MockStoreMockRecorder) GetUserByEmail(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockStore)(nil).GetUserByEmail), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetUserById mocks base method.
|
||||
func (m *MockStore) GetUserById(arg0 context.Context, arg1 uuid.UUID) (db.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -15,3 +15,8 @@ WHERE email = $1;
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetUserByEmail :one
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE email = $1;
|
@ -27,6 +27,7 @@ type Querier interface {
|
||||
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)
|
||||
GetUserByEmail(ctx context.Context, email string) (User, 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)
|
||||
|
@ -54,6 +54,27 @@ func (q *Queries) GetPasswordByEmail(ctx context.Context, email string) (string,
|
||||
return password, err
|
||||
}
|
||||
|
||||
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||
SELECT id, index_id, email, password, fullname, created_at, updated_at
|
||||
FROM users
|
||||
WHERE email = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserByEmail, email)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.IndexID,
|
||||
&i.Email,
|
||||
&i.Password,
|
||||
&i.Fullname,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserById = `-- name: GetUserById :one
|
||||
SELECT id, index_id, email, password, fullname, created_at, updated_at
|
||||
FROM users
|
||||
|
3
dev.env
3
dev.env
@ -7,3 +7,6 @@ DB_PORT=5432
|
||||
|
||||
DB_SOURCE = postgresql://postgres:awksed123@localhost:5432/nice_pos?sslmode=disable
|
||||
SERVER_ADDRESS = 0.0.0.0:8888
|
||||
|
||||
TOKEN_SYMMETRIC_KEY=75629266996751511372336382467976
|
||||
TOKEN_DURATION = 6h
|
||||
|
6
go.mod
6
go.mod
@ -3,10 +3,13 @@ module git.nochill.in/nochill/naice_pos
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/gin-gonic/gin v1.9.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/lib/pq v1.10.7
|
||||
github.com/o1egl/paseto v1.0.0
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
@ -15,6 +18,8 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
|
||||
github.com/bytedance/sonic v1.8.3 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
@ -34,6 +39,7 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
|
15
go.sum
15
go.sum
@ -38,6 +38,13 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||
github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU=
|
||||
github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 h1:1DcvRPZOdbQRg5nAHt2jrc5QbV0AGuhDdfQI6gXjiFE=
|
||||
github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU=
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.8.3 h1:pf6fGl5eqWYKkx1RcD4qpuX+BIUaduv/wTm5ekWJ80M=
|
||||
github.com/bytedance/sonic v1.8.3/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
@ -55,6 +62,8 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht
|
||||
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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@ -177,8 +186,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0=
|
||||
github.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
|
||||
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@ -234,6 +247,7 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@ -332,6 +346,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
5
main.go
5
main.go
@ -21,7 +21,10 @@ func main() {
|
||||
}
|
||||
|
||||
store := db.NewStore(dbConn)
|
||||
server := api.NewServer(store)
|
||||
server, err := api.NewServer(config, store)
|
||||
if err != nil {
|
||||
log.Fatal("cannot make server: ", err)
|
||||
}
|
||||
|
||||
err = server.Start(config.ServerAddress)
|
||||
if err != nil {
|
||||
|
58
token/jwt_maker.go
Normal file
58
token/jwt_maker.go
Normal file
@ -0,0 +1,58 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
const minSecretKeySize = 32
|
||||
|
||||
type JWTMaker struct {
|
||||
secretKey string
|
||||
}
|
||||
|
||||
func NewJWTMaker(secretKey string) (Maker, error) {
|
||||
if len(secretKey) < minSecretKeySize {
|
||||
return nil, fmt.Errorf("Invalid key: must be at least %d characters", minSecretKeySize)
|
||||
}
|
||||
|
||||
return &JWTMaker{secretKey}, nil
|
||||
}
|
||||
|
||||
func (maker *JWTMaker) CreateToken(email string, duration time.Duration) (string, error) {
|
||||
payload, err := NewPayload(email, duration)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
|
||||
return jwtToken.SignedString([]byte(maker.secretKey))
|
||||
}
|
||||
|
||||
func (maker *JWTMaker) VerifyToken(token string) (*Payload, error) {
|
||||
keyFunc := func(token *jwt.Token) (interface{}, error) {
|
||||
_, ok := token.Method.(*jwt.SigningMethodHMAC)
|
||||
if !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
return []byte(maker.secretKey), nil
|
||||
}
|
||||
jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc)
|
||||
if err != nil {
|
||||
verr, ok := err.(*jwt.ValidationError)
|
||||
if ok && errors.Is(verr.Inner, ErrExpiredToken) {
|
||||
return nil, ErrExpiredToken
|
||||
}
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
payload, ok := jwtToken.Claims.(*Payload)
|
||||
if !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
65
token/jwt_maker_test.go
Normal file
65
token/jwt_maker_test.go
Normal file
@ -0,0 +1,65 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.nochill.in/nochill/naice_pos/util"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestJWTMaker(t *testing.T) {
|
||||
maker, err := NewJWTMaker(util.RandomString(32))
|
||||
require.NoError(t, err)
|
||||
|
||||
email := util.RandomEmail()
|
||||
duration := time.Minute
|
||||
|
||||
issuedAt := time.Now()
|
||||
expiredAt := issuedAt.Add(duration)
|
||||
|
||||
token, err := maker.CreateToken(email, duration)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, token)
|
||||
|
||||
payload, err := maker.VerifyToken(token)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, payload)
|
||||
|
||||
require.NotZero(t, payload.ID)
|
||||
require.Equal(t, email, payload.Email)
|
||||
require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second)
|
||||
require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second)
|
||||
}
|
||||
|
||||
func TestExpiredToken(t *testing.T) {
|
||||
maker, err := NewJWTMaker(util.RandomString(32))
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := maker.CreateToken(util.RandomEmail(), -time.Minute)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, token)
|
||||
|
||||
payload, err := maker.VerifyToken(token)
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, ErrExpiredToken.Error())
|
||||
require.Nil(t, payload)
|
||||
}
|
||||
|
||||
func TestInvalidJWTTokenAlgNone(t *testing.T) {
|
||||
payload, err := NewPayload(util.RandomEmail(), time.Minute)
|
||||
require.NoError(t, err)
|
||||
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodNone, payload)
|
||||
token, err := jwtToken.SignedString(jwt.UnsafeAllowNoneSignatureType)
|
||||
require.NoError(t, err)
|
||||
|
||||
maker, err := NewJWTMaker(util.RandomString(32))
|
||||
require.NoError(t, err)
|
||||
|
||||
payload, err = maker.VerifyToken(token)
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, ErrInvalidToken.Error())
|
||||
require.Nil(t, payload)
|
||||
}
|
10
token/maker.go
Normal file
10
token/maker.go
Normal file
@ -0,0 +1,10 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Maker interface {
|
||||
CreateToken(email string, duration time.Duration) (string, error)
|
||||
VerifyToken(token string) (*Payload, error)
|
||||
}
|
53
token/paseto_maker.go
Normal file
53
token/paseto_maker.go
Normal file
@ -0,0 +1,53 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/aead/chacha20poly1305"
|
||||
"github.com/o1egl/paseto"
|
||||
)
|
||||
|
||||
type PasetoMaker struct {
|
||||
paseto *paseto.V2
|
||||
symmetricKey []byte
|
||||
}
|
||||
|
||||
func NewPasetoMaker(symmetricKey string) (Maker, error) {
|
||||
if len(symmetricKey) != chacha20poly1305.KeySize {
|
||||
return nil, fmt.Errorf("invalid key size: must be exactly %d characters", chacha20poly1305.KeySize)
|
||||
}
|
||||
|
||||
maker := &PasetoMaker{
|
||||
paseto: paseto.NewV2(),
|
||||
symmetricKey: []byte(symmetricKey),
|
||||
}
|
||||
|
||||
return maker, nil
|
||||
}
|
||||
|
||||
func (maker *PasetoMaker) CreateToken(email string, duration time.Duration) (string, error) {
|
||||
payload, err := NewPayload(email, duration)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token, err := maker.paseto.Encrypt(maker.symmetricKey, payload, nil)
|
||||
return token, err
|
||||
}
|
||||
|
||||
func (maker *PasetoMaker) VerifyToken(token string) (*Payload, error) {
|
||||
payload := &Payload{}
|
||||
|
||||
err := maker.paseto.Decrypt(token, maker.symmetricKey, payload, nil)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
err = payload.Valid()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
47
token/paseto_maker_test.go
Normal file
47
token/paseto_maker_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.nochill.in/nochill/naice_pos/util"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPasetoMaker(t *testing.T) {
|
||||
maker, err := NewPasetoMaker(util.RandomString(32))
|
||||
require.NoError(t, err)
|
||||
|
||||
email := util.RandomEmail()
|
||||
duration := time.Minute
|
||||
|
||||
issuedAt := time.Now()
|
||||
expiredAt := issuedAt.Add(duration)
|
||||
|
||||
token, err := maker.CreateToken(email, duration)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, token)
|
||||
|
||||
payload, err := maker.VerifyToken(token)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, payload)
|
||||
|
||||
require.NotZero(t, payload.ID)
|
||||
require.Equal(t, email, payload.Email)
|
||||
require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second)
|
||||
require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second)
|
||||
}
|
||||
|
||||
func TestExpiredPasetoToken(t *testing.T) {
|
||||
maker, err := NewPasetoMaker(util.RandomString(32))
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := maker.CreateToken(util.RandomEmail(), -time.Minute)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, token)
|
||||
|
||||
payload, err := maker.VerifyToken(token)
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, ErrExpiredToken.Error())
|
||||
require.Nil(t, payload)
|
||||
}
|
42
token/payload.go
Normal file
42
token/payload.go
Normal file
@ -0,0 +1,42 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrExpiredToken = errors.New("token has expired")
|
||||
ErrInvalidToken = errors.New("token is invalid")
|
||||
)
|
||||
|
||||
type Payload struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
ExpiredAt time.Time `json:"expired_at"`
|
||||
}
|
||||
|
||||
func NewPayload(email string, duration time.Duration) (*Payload, error) {
|
||||
tokenID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload := &Payload{
|
||||
ID: tokenID,
|
||||
Email: email,
|
||||
IssuedAt: time.Now(),
|
||||
ExpiredAt: time.Now().Add(duration),
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (payload *Payload) Valid() error {
|
||||
if time.Now().After(payload.ExpiredAt) {
|
||||
return ErrExpiredToken
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,11 +1,17 @@
|
||||
package util
|
||||
|
||||
import "github.com/spf13/viper"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DBDriver string `mapstructure:"DB_TYPE"`
|
||||
DBSource string `mapstructure:"DB_SOURCE"`
|
||||
ServerAddress string `mapstructure:"SERVER_ADDRESS"`
|
||||
TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"`
|
||||
TokenDuration time.Duration `mapstructure:"TOKEN_DURATION"`
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (config Config, err error) {
|
||||
|
Loading…
Reference in New Issue
Block a user