add user authentication

This commit is contained in:
nochill 2023-03-14 17:39:40 +07:00
parent 93f570f332
commit 4738c8c590
20 changed files with 482 additions and 21 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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 {
store db.Store
router *gin.Engine
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 {

View File

@ -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)
}

View File

@ -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)

View File

@ -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()

View File

@ -15,3 +15,8 @@ WHERE email = $1;
SELECT *
FROM users
WHERE id = $1;
-- name: GetUserByEmail :one
SELECT *
FROM users
WHERE email = $1;

View File

@ -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)

View File

@ -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

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -17,11 +17,14 @@ func main() {
}
dbConn, err := sql.Open(config.DBDriver, config.DBSource)
if err != nil {
log.Fatal("cannot connect to db:", err)
log.Fatal("cannot connect to db: ", err)
}
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
View 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
View 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
View 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
View 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
}

View 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
View 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
}

View File

@ -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"`
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) {