Add login logic (token) (#42)

This commit is contained in:
itsscb 2023-09-28 00:17:03 +02:00 committed by GitHub
parent d1cdce72ed
commit e26f46b5d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1206 additions and 239 deletions

View File

@ -2,11 +2,15 @@ package api
import ( import (
"database/sql" "database/sql"
"errors"
"fmt"
"net/http" "net/http"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
db "github.com/itsscb/df/db/sqlc" db "github.com/itsscb/df/db/sqlc"
"github.com/itsscb/df/token"
"golang.org/x/exp/slog"
) )
type createAccountRequest struct { type createAccountRequest struct {
@ -51,13 +55,6 @@ func (server *Server) createAccount(ctx *gin.Context) {
}, },
} }
// if req.PrivacyAccepted {
// arg.PrivacyAcceptedDate = sql.NullTime{
// Valid: true,
// Time: time.Now(),
// }
// }
account, err := server.store.CreateAccountTx(ctx, arg) account, err := server.store.CreateAccountTx(ctx, arg)
if err != nil { if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err)) ctx.JSON(http.StatusInternalServerError, errorResponse(err))
@ -90,12 +87,19 @@ func (server *Server) getAccount(ctx *gin.Context) {
return return
} }
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
if account.Email != authPayload.Email {
err := errors.New("account doesn't belong to the authenticated user")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}
ctx.JSON(http.StatusOK, account) ctx.JSON(http.StatusOK, account)
} }
type listAccountRequest struct { type listAccountRequest struct {
PageID int32 `form:"pageid" binding:"required,min=1"` PageID int32 `form:"page_id" binding:"required,min=1"`
PageSize int32 `form:"pagesize" binding:"required,min=5,max=50"` PageSize int32 `form:"page_size" binding:"required,min=5,max=50"`
} }
func (server *Server) listAccounts(ctx *gin.Context) { func (server *Server) listAccounts(ctx *gin.Context) {
@ -106,6 +110,26 @@ func (server *Server) listAccounts(ctx *gin.Context) {
return return
} }
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
slog.Error("auth", "payload", fmt.Sprintf("%#v", authPayload))
account, err := server.store.GetAccountByEmail(ctx, authPayload.Email)
if err != nil {
if err == sql.ErrNoRows {
ctx.JSON(http.StatusNotFound, errorResponse(err))
return
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
if account.PermissionLevel < 1 {
err := errors.New("only for admin users")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}
arg := db.ListAccountsParams{ arg := db.ListAccountsParams{
Limit: req.PageSize, Limit: req.PageSize,
Offset: (req.PageID - 1) * req.PageSize, Offset: (req.PageID - 1) * req.PageSize,
@ -133,7 +157,20 @@ func (server *Server) updateAccountPrivacy(ctx *gin.Context) {
return return
} }
account, err := server.store.UpdateAccountPrivacyTx(ctx, db.UpdateAccountPrivacyTxParams(req)) account, err := server.store.GetAccount(ctx, req.ID)
if err != nil {
ctx.JSON(http.StatusNotFound, errorResponse(err))
return
}
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
if account.Email != authPayload.Email {
err := errors.New("account doesn't belong to the authenticated user")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}
account, err = server.store.UpdateAccountPrivacyTx(ctx, db.UpdateAccountPrivacyTxParams(req))
if err != nil { if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err)) ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return return
@ -164,6 +201,19 @@ func (server *Server) updateAccount(ctx *gin.Context) {
return return
} }
account, err := server.store.GetAccount(ctx, req.ID)
if err != nil {
ctx.JSON(http.StatusNotFound, errorResponse(err))
return
}
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
if account.Email != authPayload.Email {
err := errors.New("account doesn't belong to the authenticated user")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}
arg := db.UpdateAccountTxParams{ arg := db.UpdateAccountTxParams{
ID: req.ID, ID: req.ID,
Changer: req.Changer, Changer: req.Changer,
@ -209,7 +259,7 @@ func (server *Server) updateAccount(ctx *gin.Context) {
}, },
} }
account, err := server.store.UpdateAccountTx(ctx, arg) account, err = server.store.UpdateAccountTx(ctx, arg)
if err != nil { if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err)) ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return return

View File

@ -14,6 +14,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
mockdb "github.com/itsscb/df/db/mock" mockdb "github.com/itsscb/df/db/mock"
db "github.com/itsscb/df/db/sqlc" db "github.com/itsscb/df/db/sqlc"
"github.com/itsscb/df/token"
"github.com/itsscb/df/util" "github.com/itsscb/df/util"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/mock/gomock" "go.uber.org/mock/gomock"
@ -27,6 +28,7 @@ func TestCreateAccountAPI(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
body gin.H body gin.H
setupAuth func(t *testing.T, request *http.Request, tokenMaker token.Maker)
buildStubs func(store *mockdb.MockStore) buildStubs func(store *mockdb.MockStore)
checkResponse func(recoder *httptest.ResponseRecorder) checkResponse func(recoder *httptest.ResponseRecorder)
}{ }{
@ -46,6 +48,9 @@ func TestCreateAccountAPI(t *testing.T) {
"phone": account.Phone.String, "phone": account.Phone.String,
"creator": account.Creator, "creator": account.Creator,
}, },
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
arg := db.CreateAccountTxParams{ arg := db.CreateAccountTxParams{
Passwordhash: account.Passwordhash, Passwordhash: account.Passwordhash,
@ -75,11 +80,24 @@ func TestCreateAccountAPI(t *testing.T) {
// { // {
// name: "NoAuthorization", // name: "NoAuthorization",
// body: gin.H{ // body: gin.H{
// "currency": account.Currency, // "passwordhash": account.Passwordhash,
// "privacy_accepted": account.PrivacyAccepted.Bool,
// "firstname": account.Firstname,
// "lastname": account.Lastname,
// "birthday": account.Birthday,
// "email": account.Email,
// "city": account.City,
// "zip": account.Zip,
// "street": account.Street,
// "country": account.Country,
// "phone": account.Phone.String,
// "creator": account.Creator,
// },
// setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
// }, // },
// buildStubs: func(store *mockdb.MockStore) { // buildStubs: func(store *mockdb.MockStore) {
// store.EXPECT(). // store.EXPECT().
// CreateAccount(gomock.Any(), gomock.Any()). // CreateAccountTx(gomock.Any(), gomock.Any()).
// Times(0) // Times(0)
// }, // },
// checkResponse: func(recorder *httptest.ResponseRecorder) { // checkResponse: func(recorder *httptest.ResponseRecorder) {
@ -91,6 +109,9 @@ func TestCreateAccountAPI(t *testing.T) {
body: gin.H{ body: gin.H{
"email": account.Email, "email": account.Email,
}, },
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
store.EXPECT(). store.EXPECT().
CreateAccountTx(gomock.Any(), gomock.Any()). CreateAccountTx(gomock.Any(), gomock.Any()).
@ -117,6 +138,9 @@ func TestCreateAccountAPI(t *testing.T) {
"phone": account.Phone.String, "phone": account.Phone.String,
"creator": account.Creator, "creator": account.Creator,
}, },
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
store.EXPECT(). store.EXPECT().
CreateAccountTx(gomock.Any(), gomock.Any()). CreateAccountTx(gomock.Any(), gomock.Any()).
@ -139,7 +163,9 @@ func TestCreateAccountAPI(t *testing.T) {
store := mockdb.NewMockStore(ctrl) store := mockdb.NewMockStore(ctrl)
tc.buildStubs(store) tc.buildStubs(store)
server := NewServer(config, store) server, err := NewServer(config, store)
require.NoError(t, err)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
// Marshal body data to JSON // Marshal body data to JSON
@ -150,6 +176,7 @@ func TestCreateAccountAPI(t *testing.T) {
request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data)) request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
require.NoError(t, err) require.NoError(t, err)
tc.setupAuth(t, request, server.tokenMaker)
server.router.ServeHTTP(recorder, request) server.router.ServeHTTP(recorder, request)
tc.checkResponse(recorder) tc.checkResponse(recorder)
}) })
@ -162,12 +189,16 @@ func TestGetAccountAPI(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
accountID int64 accountID int64
setupAuth func(t *testing.T, request *http.Request, tokenMaker token.Maker)
buildStubs func(store *mockdb.MockStore) buildStubs func(store *mockdb.MockStore)
checkResponse func(t *testing.T, recoder *httptest.ResponseRecorder) checkResponse func(t *testing.T, recoder *httptest.ResponseRecorder)
}{ }{
{ {
name: "OK", name: "OK",
accountID: account.ID, accountID: account.ID,
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
store.EXPECT(). store.EXPECT().
GetAccount(gomock.Any(), gomock.Eq(account.ID)). GetAccount(gomock.Any(), gomock.Eq(account.ID)).
@ -179,37 +210,42 @@ func TestGetAccountAPI(t *testing.T) {
requireBodyMatchAccount(t, recorder.Body, account) requireBodyMatchAccount(t, recorder.Body, account)
}, },
}, },
// { {
// name: "UnauthorizedUser", name: "UnauthorizedUser",
// accountID: account.ID, accountID: account.ID,
// setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
// addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "unauthorized_user", time.Minute) addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "UnauthorizedUser", time.Minute)
// }, },
// buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
// store.EXPECT(). store.EXPECT().
// GetAccount(gomock.Any(), gomock.Eq(account.ID)). GetAccount(gomock.Any(), gomock.Eq(account.ID)).
// Times(1). Times(1).
// Return(account, nil) Return(account, nil)
// }, },
// checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
// require.Equal(t, http.StatusUnauthorized, recorder.Code) require.Equal(t, http.StatusUnauthorized, recorder.Code)
// }, },
// }, },
// { {
// name: "NoAuthorization", name: "NoAuthorization",
// accountID: account.ID, accountID: account.ID,
// buildStubs: func(store *mockdb.MockStore) { setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
// store.EXPECT(). },
// GetAccount(gomock.Any(), gomock.Any()). buildStubs: func(store *mockdb.MockStore) {
// Times(0) store.EXPECT().
// }, GetAccount(gomock.Any(), gomock.Any()).
// checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { Times(0)
// require.Equal(t, http.StatusUnauthorized, recorder.Code) },
// }, checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
// }, require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
{ {
name: "NotFound", name: "NotFound",
accountID: account.ID, accountID: account.ID,
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
store.EXPECT(). store.EXPECT().
GetAccount(gomock.Any(), gomock.Eq(account.ID)). GetAccount(gomock.Any(), gomock.Eq(account.ID)).
@ -223,6 +259,9 @@ func TestGetAccountAPI(t *testing.T) {
{ {
name: "InternalError", name: "InternalError",
accountID: account.ID, accountID: account.ID,
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
store.EXPECT(). store.EXPECT().
GetAccount(gomock.Any(), gomock.Eq(account.ID)). GetAccount(gomock.Any(), gomock.Eq(account.ID)).
@ -236,6 +275,9 @@ func TestGetAccountAPI(t *testing.T) {
{ {
name: "InvalidID", name: "InvalidID",
accountID: 0, accountID: 0,
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
store.EXPECT(). store.EXPECT().
GetAccount(gomock.Any(), gomock.Any()). GetAccount(gomock.Any(), gomock.Any()).
@ -257,13 +299,15 @@ func TestGetAccountAPI(t *testing.T) {
store := mockdb.NewMockStore(ctrl) store := mockdb.NewMockStore(ctrl)
tc.buildStubs(store) tc.buildStubs(store)
server := NewServer(config, store) server, err := NewServer(config, store)
require.NoError(t, err)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
url := fmt.Sprintf("/accounts/%d", tc.accountID) url := fmt.Sprintf("/accounts/%d", tc.accountID)
request, err := http.NewRequest(http.MethodGet, url, nil) request, err := http.NewRequest(http.MethodGet, url, nil)
require.NoError(t, err) require.NoError(t, err)
tc.setupAuth(t, request, server.tokenMaker)
server.router.ServeHTTP(recorder, request) server.router.ServeHTTP(recorder, request)
tc.checkResponse(t, recorder) tc.checkResponse(t, recorder)
}) })
@ -273,67 +317,81 @@ func TestGetAccountAPI(t *testing.T) {
func TestUpdateAccountTxAPI(t *testing.T) { func TestUpdateAccountTxAPI(t *testing.T) {
account := randomAccount() account := randomAccount()
changer := util.RandomName() changer := util.RandomName()
newPassword := util.RandomString(30) // newPassword := util.RandomString(30)
newEmail := util.RandomEmail() newLastname := util.RandomName()
testCases := []struct { testCases := []struct {
name string name string
body gin.H body gin.H
accountID string accountID string
setupAuth func(t *testing.T, request *http.Request, tokenMaker token.Maker)
buildStubs func(store *mockdb.MockStore) buildStubs func(store *mockdb.MockStore)
checkResponse func(recoder *httptest.ResponseRecorder) checkResponse func(recoder *httptest.ResponseRecorder)
}{ }{
// {
// name: "OK_PasswordHash",
// body: gin.H{
// "id": account.ID,
// "passwordhash": newPassword,
// "changer": changer,
// },
// setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
// addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
// },
// buildStubs: func(store *mockdb.MockStore) {
// var err error
// accountTemp := account
// accountTemp.Passwordhash, err = util.HashPassword(newPassword)
// require.NoError(t, err)
// accountTemp.Changer = changer
// arg := db.UpdateAccountTxParams{
// ID: account.ID,
// Passwordhash: sql.NullString{
// Valid: true,
// String: newPassword,
// },
// Changer: changer,
// }
// store.EXPECT().
// UpdateAccountTx(gomock.Any(), gomock.Eq(arg)).
// Times(1).
// Return(accountTemp, nil)
// },
// checkResponse: func(recorder *httptest.ResponseRecorder) {
// require.Equal(t, http.StatusOK, recorder.Code)
// accountTemp := account
// accountTemp.Passwordhash = newPassword
// accountTemp.Changer = changer
// requireBodyMatchAccount(t, recorder.Body, accountTemp)
// },
// },
{ {
name: "OK_PasswordHash", name: "OK_Lastname",
body: gin.H{ body: gin.H{
"id": account.ID, "id": account.ID,
"passwordhash": newPassword, "lastname": newLastname,
"changer": changer, "changer": changer,
},
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
}, },
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
accountTemp := account
accountTemp.Passwordhash = newPassword
accountTemp.Changer = changer
arg := db.UpdateAccountTxParams{ arg := db.UpdateAccountTxParams{
ID: account.ID, ID: account.ID,
Passwordhash: sql.NullString{ Lastname: sql.NullString{
Valid: true, Valid: true,
String: newPassword, String: newLastname,
}, },
Changer: changer, Changer: changer,
} }
store.EXPECT(). store.EXPECT().
UpdateAccountTx(gomock.Any(), gomock.Eq(arg)). GetAccount(gomock.Any(), gomock.Eq(account.ID)).
Times(1). Times(1).
Return(accountTemp, nil) Return(account, nil)
},
checkResponse: func(recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusOK, recorder.Code)
accountTemp := account
accountTemp.Passwordhash = newPassword
accountTemp.Changer = changer
requireBodyMatchAccount(t, recorder.Body, accountTemp)
},
},
{
name: "OK_Email",
body: gin.H{
"id": account.ID,
"email": newEmail,
"changer": changer,
},
buildStubs: func(store *mockdb.MockStore) {
arg := db.UpdateAccountTxParams{
ID: account.ID,
Email: sql.NullString{
Valid: true,
String: newEmail,
},
Changer: changer,
}
store.EXPECT(). store.EXPECT().
UpdateAccountTx(gomock.Any(), gomock.Eq(arg)). UpdateAccountTx(gomock.Any(), gomock.Eq(arg)).
@ -345,58 +403,32 @@ func TestUpdateAccountTxAPI(t *testing.T) {
requireBodyMatchAccount(t, recorder.Body, account) requireBodyMatchAccount(t, recorder.Body, account)
}, },
}, },
// { {
// name: "OK_PrivacyAccepted", name: "NoAuthorization",
// body: gin.H{ body: gin.H{
// "id": account.ID, "id": account.ID,
// "privacy_accepted": true, "lastname": newLastname,
// "changer": changer, "changer": changer,
// }, },
// buildStubs: func(store *mockdb.MockStore) { setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
// accountAccepted := account },
// accountAccepted.PrivacyAccepted = sql.NullBool{ buildStubs: func(store *mockdb.MockStore) {
// Valid: true, store.EXPECT().
// Bool: true, CreateAccount(gomock.Any(), gomock.Any()).
// } Times(0)
// accountAccepted.PrivacyAcceptedDate = sql.NullTime{ },
// Valid: true, checkResponse: func(recorder *httptest.ResponseRecorder) {
// Time: timestamp, require.Equal(t, http.StatusUnauthorized, recorder.Code)
// } },
},
// arg := db.UpdateAccountTxParams{
// ID: account.ID,
// PrivacyAccepted: sql.NullBool{
// Valid: true,
// Bool: true,
// },
// Changer: changer,
// }
// store.EXPECT().
// UpdateAccountTx(gomock.Any(), gomock.Eq(arg)).
// Times(1).
// Return(accountAccepted, nil)
// },
// },
// {
// name: "NoAuthorization",
// body: gin.H{
// "currency": account.Currency,
// },
// buildStubs: func(store *mockdb.MockStore) {
// store.EXPECT().
// CreateAccount(gomock.Any(), gomock.Any()).
// Times(0)
// },
// checkResponse: func(recorder *httptest.ResponseRecorder) {
// require.Equal(t, http.StatusUnauthorized, recorder.Code)
// },
// },
{ {
name: "BadRequest", name: "BadRequest",
body: gin.H{ body: gin.H{
"email": account.Email, "email": account.Email,
}, },
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
store.EXPECT(). store.EXPECT().
CreateAccount(gomock.Any(), gomock.Any()). CreateAccount(gomock.Any(), gomock.Any()).
@ -419,7 +451,9 @@ func TestUpdateAccountTxAPI(t *testing.T) {
store := mockdb.NewMockStore(ctrl) store := mockdb.NewMockStore(ctrl)
tc.buildStubs(store) tc.buildStubs(store)
server := NewServer(config, store) server, err := NewServer(config, store)
require.NoError(t, err)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
// Marshal body data to JSON // Marshal body data to JSON
@ -430,6 +464,7 @@ func TestUpdateAccountTxAPI(t *testing.T) {
request, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data)) request, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data))
require.NoError(t, err) require.NoError(t, err)
tc.setupAuth(t, request, server.tokenMaker)
server.router.ServeHTTP(recorder, request) server.router.ServeHTTP(recorder, request)
tc.checkResponse(recorder) tc.checkResponse(recorder)
}) })
@ -443,6 +478,7 @@ func TestListAccountsAPI(t *testing.T) {
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
accounts[i] = randomAccount() accounts[i] = randomAccount()
} }
account := accounts[1]
type Query struct { type Query struct {
pageID int pageID int
@ -452,6 +488,7 @@ func TestListAccountsAPI(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
query Query query Query
setupAuth func(t *testing.T, request *http.Request, tokenMaker token.Maker)
buildStubs func(store *mockdb.MockStore) buildStubs func(store *mockdb.MockStore)
checkResponse func(recoder *httptest.ResponseRecorder) checkResponse func(recoder *httptest.ResponseRecorder)
}{ }{
@ -461,12 +498,23 @@ func TestListAccountsAPI(t *testing.T) {
pageID: 1, pageID: 1,
pageSize: n, pageSize: n,
}, },
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
arg := db.ListAccountsParams{ arg := db.ListAccountsParams{
Limit: int32(n), Limit: int32(n),
Offset: 0, Offset: 0,
} }
accountAdmin := account
accountAdmin.PermissionLevel = 1
store.EXPECT().
GetAccountByEmail(gomock.Any(), gomock.Eq(account.Email)).
Times(1).
Return(accountAdmin, nil)
store.EXPECT(). store.EXPECT().
ListAccounts(gomock.Any(), gomock.Eq(arg)). ListAccounts(gomock.Any(), gomock.Eq(arg)).
Times(1). Times(1).
@ -477,24 +525,29 @@ func TestListAccountsAPI(t *testing.T) {
requireBodyMatchAccounts(t, recorder.Body, accounts) requireBodyMatchAccounts(t, recorder.Body, accounts)
}, },
}, },
// { {
// name: "NoAuthorization", name: "NoAuthorization",
// query: Query{ query: Query{
// pageID: 1, pageID: 1,
// pageSize: n, pageSize: n,
// }, },
// buildStubs: func(store *mockdb.MockStore) { setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
// store.EXPECT(). },
// ListAccounts(gomock.Any(), gomock.Any()). buildStubs: func(store *mockdb.MockStore) {
// Times(0) store.EXPECT().
// }, ListAccounts(gomock.Any(), gomock.Any()).
// checkResponse: func(recorder *httptest.ResponseRecorder) { Times(0)
// require.Equal(t, http.StatusUnauthorized, recorder.Code) },
// }, checkResponse: func(recorder *httptest.ResponseRecorder) {
// }, require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
{ {
name: "EmptyQuery", name: "EmptyQuery",
query: Query{}, query: Query{},
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
store.EXPECT(). store.EXPECT().
ListAccounts(gomock.Any(), gomock.Any()). ListAccounts(gomock.Any(), gomock.Any()).
@ -510,6 +563,9 @@ func TestListAccountsAPI(t *testing.T) {
pageID: -1, pageID: -1,
pageSize: n, pageSize: n,
}, },
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
store.EXPECT(). store.EXPECT().
ListAccounts(gomock.Any(), gomock.Any()). ListAccounts(gomock.Any(), gomock.Any()).
@ -525,6 +581,9 @@ func TestListAccountsAPI(t *testing.T) {
pageID: 1, pageID: 1,
pageSize: 100000, pageSize: 100000,
}, },
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
store.EXPECT(). store.EXPECT().
ListAccounts(gomock.Any(), gomock.Any()). ListAccounts(gomock.Any(), gomock.Any()).
@ -546,7 +605,9 @@ func TestListAccountsAPI(t *testing.T) {
store := mockdb.NewMockStore(ctrl) store := mockdb.NewMockStore(ctrl)
tc.buildStubs(store) tc.buildStubs(store)
server := NewServer(config, store) server, err := NewServer(config, store)
require.NoError(t, err)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
url := "/accounts" url := "/accounts"
@ -555,10 +616,11 @@ func TestListAccountsAPI(t *testing.T) {
// Add query parameters to request URL // Add query parameters to request URL
q := request.URL.Query() q := request.URL.Query()
q.Add("pageid", fmt.Sprintf("%d", tc.query.pageID)) q.Add("page_id", fmt.Sprintf("%d", tc.query.pageID))
q.Add("pagesize", fmt.Sprintf("%d", tc.query.pageSize)) q.Add("page_size", fmt.Sprintf("%d", tc.query.pageSize))
request.URL.RawQuery = q.Encode() request.URL.RawQuery = q.Encode()
tc.setupAuth(t, request, server.tokenMaker)
server.router.ServeHTTP(recorder, request) server.router.ServeHTTP(recorder, request)
tc.checkResponse(recorder) tc.checkResponse(recorder)
}) })
@ -572,6 +634,7 @@ func TestUpdateAccountPrivacyTxAPI(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
body gin.H body gin.H
setupAuth func(t *testing.T, request *http.Request, tokenMaker token.Maker)
buildStubs func(store *mockdb.MockStore) buildStubs func(store *mockdb.MockStore)
checkResponse func(recoder *httptest.ResponseRecorder) checkResponse func(recoder *httptest.ResponseRecorder)
}{ }{
@ -582,6 +645,9 @@ func TestUpdateAccountPrivacyTxAPI(t *testing.T) {
"changer": changer, "changer": changer,
"privacy_accepted": true, "privacy_accepted": true,
}, },
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
arg := db.UpdateAccountPrivacyTxParams{ arg := db.UpdateAccountPrivacyTxParams{
ID: account.ID, ID: account.ID,
@ -594,6 +660,11 @@ func TestUpdateAccountPrivacyTxAPI(t *testing.T) {
account2.PrivacyAccepted.Bool = true account2.PrivacyAccepted.Bool = true
account2.Changer = changer account2.Changer = changer
store.EXPECT().
GetAccount(gomock.Any(), gomock.Eq(account.ID)).
Times(1).
Return(account, nil)
store.EXPECT(). store.EXPECT().
UpdateAccountPrivacyTx(gomock.Any(), gomock.Eq(arg)). UpdateAccountPrivacyTx(gomock.Any(), gomock.Eq(arg)).
Times(1). Times(1).
@ -621,6 +692,9 @@ func TestUpdateAccountPrivacyTxAPI(t *testing.T) {
"changer": changer, "changer": changer,
"privacy_accepted": false, "privacy_accepted": false,
}, },
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
arg := db.UpdateAccountPrivacyTxParams{ arg := db.UpdateAccountPrivacyTxParams{
ID: account.ID, ID: account.ID,
@ -635,6 +709,11 @@ func TestUpdateAccountPrivacyTxAPI(t *testing.T) {
account2.PrivacyAcceptedDate.Time = time.Time{} account2.PrivacyAcceptedDate.Time = time.Time{}
account2.Changer = changer account2.Changer = changer
store.EXPECT().
GetAccount(gomock.Any(), gomock.Eq(account.ID)).
Times(1).
Return(account, nil)
store.EXPECT(). store.EXPECT().
UpdateAccountPrivacyTx(gomock.Any(), gomock.Eq(arg)). UpdateAccountPrivacyTx(gomock.Any(), gomock.Eq(arg)).
Times(1). Times(1).
@ -656,11 +735,18 @@ func TestUpdateAccountPrivacyTxAPI(t *testing.T) {
}, },
}, { }, {
name: "OK", name: "InvalidRequest",
body: gin.H{ body: gin.H{
"id": account.ID, "id": account.ID,
}, },
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, account.Email, time.Minute)
},
buildStubs: func(store *mockdb.MockStore) { buildStubs: func(store *mockdb.MockStore) {
store.EXPECT().
GetAccount(gomock.Any(), gomock.Any()).
Times(0)
store.EXPECT(). store.EXPECT().
UpdateAccountPrivacyTx(gomock.Any(), gomock.Any()). UpdateAccountPrivacyTx(gomock.Any(), gomock.Any()).
Times(0) Times(0)
@ -681,7 +767,9 @@ func TestUpdateAccountPrivacyTxAPI(t *testing.T) {
store := mockdb.NewMockStore(ctrl) store := mockdb.NewMockStore(ctrl)
tc.buildStubs(store) tc.buildStubs(store)
server := NewServer(config, store) server, err := NewServer(config, store)
require.NoError(t, err)
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
// Marshal body data to JSON // Marshal body data to JSON
@ -692,6 +780,7 @@ func TestUpdateAccountPrivacyTxAPI(t *testing.T) {
request, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data)) request, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data))
require.NoError(t, err) require.NoError(t, err)
tc.setupAuth(t, request, server.tokenMaker)
server.router.ServeHTTP(recorder, request) server.router.ServeHTTP(recorder, request)
tc.checkResponse(recorder) tc.checkResponse(recorder)
}) })
@ -699,9 +788,12 @@ func TestUpdateAccountPrivacyTxAPI(t *testing.T) {
} }
func randomAccount() db.Account { func randomAccount() db.Account {
password := util.RandomString(6)
hashedPassword, _ := util.HashPassword(password)
acc := db.Account{ acc := db.Account{
ID: util.RandomInt(1, 1000), ID: util.RandomInt(1, 1000),
Passwordhash: util.RandomString(250), Passwordhash: hashedPassword,
Firstname: util.RandomName(), Firstname: util.RandomName(),
Lastname: util.RandomName(), Lastname: util.RandomName(),
Email: util.RandomEmail(), Email: util.RandomEmail(),

View File

@ -3,6 +3,7 @@ package api
import ( import (
"os" "os"
"testing" "testing"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/itsscb/df/util" "github.com/itsscb/df/util"
@ -12,7 +13,10 @@ var config util.Config
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
config = util.Config{ config = util.Config{
Environment: "production", Environment: "production",
TokenSymmetricKey: "12345678901234567890123456789012",
AccessTokenDuration: time.Minute * 1,
RefreshTokenDuration: time.Minute * 2,
} }
gin.SetMode(gin.TestMode) gin.SetMode(gin.TestMode)

54
api/middleware.go Normal file
View File

@ -0,0 +1,54 @@
package api
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/itsscb/df/token"
)
const (
authorizationHeaderKey = "authorization"
authorizationTypeBearer = "bearer"
authorizationPayloadKey = "authorization_payload"
)
// AuthMiddleware creates a gin middleware for authorization
func authMiddleware(tokenMaker token.Maker) gin.HandlerFunc {
return func(ctx *gin.Context) {
authorizationHeader := ctx.GetHeader(authorizationHeaderKey)
if len(authorizationHeader) == 0 {
err := errors.New("authorization header is not provided")
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
return
}
fields := strings.Fields(authorizationHeader)
if len(fields) < 2 {
err := errors.New("invalid authorization header format")
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
return
}
authorizationType := strings.ToLower(fields[0])
if authorizationType != authorizationTypeBearer {
err := fmt.Errorf("unsupported authorization type %s", authorizationType)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
return
}
accessToken := fields[1]
payload, err := tokenMaker.VerifyToken(accessToken)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
return
}
ctx.Set(authorizationPayloadKey, payload)
ctx.Next()
}
}

114
api/middleware_test.go Normal file
View File

@ -0,0 +1,114 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
mockdb "github.com/itsscb/df/db/mock"
"github.com/itsscb/df/token"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func addAuthorization(
t *testing.T,
request *http.Request,
tokenMaker token.Maker,
authorizationType string,
email string,
duration time.Duration,
) {
token, payload, err := tokenMaker.CreateToken(email, duration)
require.NoError(t, err)
require.NotEmpty(t, payload)
authorizationHeader := fmt.Sprintf("%s %s", authorizationType, token)
request.Header.Set(authorizationHeaderKey, authorizationHeader)
}
func TestAuthMiddleware(t *testing.T) {
testCases := []struct {
name string
setupAuth func(t *testing.T, request *http.Request, tokenMaker token.Maker)
checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder)
}{
{
name: "OK",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "user", time.Minute)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusOK, recorder.Code)
},
},
{
name: "NoAuthorization",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
{
name: "UnsupportedAuthorization",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, "unsupported", "user", time.Minute)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
{
name: "InvalidAuthorizationFormat",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, "", "user", time.Minute)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
{
name: "ExpiredToken",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "user", -time.Minute)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
store := mockdb.NewMockStore(ctrl)
server, err := NewServer(config, store)
require.NoError(t, err)
authPath := "/auth"
server.router.GET(
authPath,
authMiddleware(server.tokenMaker),
func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{})
},
)
recorder := httptest.NewRecorder()
request, err := http.NewRequest(http.MethodGet, authPath, nil)
require.NoError(t, err)
tc.setupAuth(t, request, server.tokenMaker)
server.router.ServeHTTP(recorder, request)
tc.checkResponse(t, recorder)
})
}
}

View File

@ -1,26 +1,35 @@
package api package api
import ( import (
"fmt"
"log/slog" "log/slog"
"os" "os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
db "github.com/itsscb/df/db/sqlc" db "github.com/itsscb/df/db/sqlc"
"github.com/itsscb/df/token"
"github.com/itsscb/df/util" "github.com/itsscb/df/util"
) )
// Server serves HTTP requests for df service // Server serves HTTP requests for df service
type Server struct { type Server struct {
store db.Store store db.Store
router *gin.Engine router *gin.Engine
config util.Config config util.Config
tokenMaker token.Maker
} }
// NewServer creates a new HTTP server and sets up routing // NewServer creates a new HTTP server and sets up routing
func NewServer(config util.Config, store db.Store) *Server { 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{ server := &Server{
store: store, store: store,
config: config, config: config,
tokenMaker: tokenMaker,
} }
logLevel := slog.LevelError logLevel := slog.LevelError
@ -45,14 +54,18 @@ func NewServer(config util.Config, store db.Store) *Server {
router.Use(Logger()) router.Use(Logger())
router.POST("/accounts/login", server.loginAccount)
router.POST("/tokens/renew_access", server.renewAccessToken)
router.POST("/accounts", server.createAccount) router.POST("/accounts", server.createAccount)
router.PUT("/accounts", server.updateAccount)
router.PUT("/accounts/privacy", server.updateAccountPrivacy) authRoutes := router.Group("/").Use(authMiddleware(server.tokenMaker))
router.GET("/accounts/:id", server.getAccount) authRoutes.PUT("/accounts", server.updateAccount)
router.GET("/accounts", server.listAccounts) authRoutes.PUT("/accounts/privacy", server.updateAccountPrivacy)
authRoutes.GET("/accounts/:id", server.getAccount)
authRoutes.GET("/accounts", server.listAccounts)
server.router = router server.router = router
return server return server, nil
} }
func (server *Server) Start(address string) error { func (server *Server) Start(address string) error {

93
api/session.go Normal file
View File

@ -0,0 +1,93 @@
package api
import (
"database/sql"
"errors"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
db "github.com/itsscb/df/db/sqlc"
"github.com/itsscb/df/util"
)
type loginAccountRequest struct {
Email string `json:"email" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
}
type loginAccountResponse struct {
SessionID uuid.UUID `json:"session_id"`
AccessToken string `json:"access_token"`
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
RefreshToken string `json:"refresh_token"`
RefreshTokenExpiresAt time.Time `json:"refresh_token_expires_at"`
Email string `json:"email"`
}
func (server *Server) loginAccount(ctx *gin.Context) {
var req loginAccountRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}
account, err := server.store.GetAccountByEmail(ctx, req.Email)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
ctx.JSON(http.StatusNotFound, errorResponse(err))
return
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
err = util.CheckPassword(req.Password, account.Passwordhash)
if err != nil {
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}
accessToken, accessPayload, err := server.tokenMaker.CreateToken(
account.Email,
server.config.AccessTokenDuration,
)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
refreshToken, refreshPayload, err := server.tokenMaker.CreateToken(
account.Email,
server.config.RefreshTokenDuration,
)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
session, err := server.store.CreateSession(ctx, db.CreateSessionParams{
ID: refreshPayload.ID,
Email: account.Email,
RefreshToken: refreshToken,
UserAgent: ctx.Request.UserAgent(),
ClientIp: ctx.ClientIP(),
IsBlocked: false,
ExpiresAt: refreshPayload.ExpiredAt,
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
rsp := loginAccountResponse{
SessionID: session.ID,
AccessToken: accessToken,
AccessTokenExpiresAt: accessPayload.ExpiredAt,
RefreshToken: refreshToken,
RefreshTokenExpiresAt: refreshPayload.ExpiredAt,
Email: account.Email,
}
ctx.JSON(http.StatusOK, rsp)
}

83
api/token.go Normal file
View File

@ -0,0 +1,83 @@
package api
import (
"database/sql"
"errors"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
type renewAccessTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
type renewAccessTokenResponse struct {
AccessToken string `json:"access_token"`
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
}
func (server *Server) renewAccessToken(ctx *gin.Context) {
var req renewAccessTokenRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
return
}
refreshPayload, err := server.tokenMaker.VerifyToken(req.RefreshToken)
if err != nil {
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}
session, err := server.store.GetSession(ctx, refreshPayload.ID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
ctx.JSON(http.StatusNotFound, errorResponse(err))
return
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
if session.IsBlocked {
err := fmt.Errorf("blocked session")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}
if session.Email != refreshPayload.Email {
err := fmt.Errorf("incorrect session user")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}
if session.RefreshToken != req.RefreshToken {
err := fmt.Errorf("mismatched session token")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}
if time.Now().After(session.ExpiresAt) {
err := fmt.Errorf("expired session")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
return
}
accessToken, accessPayload, err := server.tokenMaker.CreateToken(
refreshPayload.Email,
server.config.AccessTokenDuration,
)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
rsp := renewAccessTokenResponse{
AccessToken: accessToken,
AccessTokenExpiresAt: accessPayload.ExpiredAt,
}
ctx.JSON(http.StatusOK, rsp)
}

View File

@ -2,4 +2,7 @@ DB_SOURCE=postgresql://root:secret@localhost:5432/df?sslmode=disable
DB_DRIVER=postgres DB_DRIVER=postgres
SERVER_ADDRESS=0.0.0.0:8080 SERVER_ADDRESS=0.0.0.0:8080
ENVIRONMENT=development ENVIRONMENT=development
LOG_OUTPUT=text LOG_OUTPUT=text
ACCESS_TOKEN_DURATION=15m
REFRESH_TOKEN_DURATION=24h
TOKEN_SYMMETRIC_KEY=12345678901234567890123456789012

View File

@ -5,6 +5,7 @@ DROP TABLE IF EXISTS "documents";
DROP TABLE IF EXISTS "mails"; DROP TABLE IF EXISTS "mails";
DROP TABLE IF EXISTS "persons"; DROP TABLE IF EXISTS "persons";
DROP TABLE IF EXISTS "providers"; DROP TABLE IF EXISTS "providers";
DROP TABLE IF EXISTS "sessions";
DROP TABLE IF EXISTS "accounts"; DROP TABLE IF EXISTS "accounts";

View File

@ -14,6 +14,7 @@ CREATE TABLE "mails" (
CREATE TABLE "accounts" ( CREATE TABLE "accounts" (
"id" bigserial UNIQUE PRIMARY KEY NOT NULL, "id" bigserial UNIQUE PRIMARY KEY NOT NULL,
"permission_level" int NOT NULL DEFAULT 0,
"passwordhash" varchar NOT NULL, "passwordhash" varchar NOT NULL,
"firstname" varchar NOT NULL, "firstname" varchar NOT NULL,
"lastname" varchar NOT NULL, "lastname" varchar NOT NULL,
@ -26,15 +27,23 @@ CREATE TABLE "accounts" (
"zip" varchar NOT NULL, "zip" varchar NOT NULL,
"street" varchar NOT NULL, "street" varchar NOT NULL,
"country" varchar NOT NULL, "country" varchar NOT NULL,
"token" varchar,
"token_valid" boolean DEFAULT false,
"token_expiration" timestamptz NOT NULL DEFAULT (now()),
"creator" varchar NOT NULL, "creator" varchar NOT NULL,
"created" timestamptz NOT NULL DEFAULT (now()), "created" timestamptz NOT NULL DEFAULT (now()),
"changer" varchar NOT NULL, "changer" varchar NOT NULL,
"changed" timestamptz NOT NULL DEFAULT (now()) "changed" timestamptz NOT NULL DEFAULT (now())
); );
CREATE TABLE "sessions" (
"id" uuid UNIQUE PRIMARY KEY NOT NULL,
"email" varchar NOT NULL,
"user_agent" varchar NOT NULL,
"client_ip" varchar NOT NULL,
"refresh_token" varchar NOT NULL,
"is_blocked" boolean NOT NULL DEFAULT false,
"expires_at" timestamptz NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE TABLE "persons" ( CREATE TABLE "persons" (
"id" bigserial UNIQUE PRIMARY KEY NOT NULL, "id" bigserial UNIQUE PRIMARY KEY NOT NULL,
"account_id" bigint NOT NULL, "account_id" bigint NOT NULL,
@ -73,8 +82,8 @@ CREATE TABLE "payments" (
"account_id" bigint NOT NULL, "account_id" bigint NOT NULL,
"payment_category" varchar NOT NULL, "payment_category" varchar NOT NULL,
"bankname" varchar, "bankname" varchar,
"iban" varchar, "IBAN" varchar,
"bic" varchar, "BIC" varchar,
"paypal_account" varchar, "paypal_account" varchar,
"paypal_id" varchar, "paypal_id" varchar,
"payment_system" varchar, "payment_system" varchar,
@ -123,6 +132,8 @@ CREATE TABLE "returnsLog" (
"changed" timestamptz NOT NULL DEFAULT (now()) "changed" timestamptz NOT NULL DEFAULT (now())
); );
ALTER TABLE "sessions" ADD FOREIGN KEY ("email") REFERENCES "accounts" ("email");
ALTER TABLE "persons" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id"); ALTER TABLE "persons" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id");
ALTER TABLE "documents" ADD FOREIGN KEY ("person_id") REFERENCES "persons" ("id"); ALTER TABLE "documents" ADD FOREIGN KEY ("person_id") REFERENCES "persons" ("id");

View File

@ -12,6 +12,7 @@ import (
context "context" context "context"
reflect "reflect" reflect "reflect"
uuid "github.com/google/uuid"
db "github.com/itsscb/df/db/sqlc" db "github.com/itsscb/df/db/sqlc"
gomock "go.uber.org/mock/gomock" gomock "go.uber.org/mock/gomock"
) )
@ -189,6 +190,21 @@ func (mr *MockStoreMockRecorder) CreateReturnsLog(arg0, arg1 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateReturnsLog", reflect.TypeOf((*MockStore)(nil).CreateReturnsLog), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateReturnsLog", reflect.TypeOf((*MockStore)(nil).CreateReturnsLog), arg0, arg1)
} }
// CreateSession mocks base method.
func (m *MockStore) CreateSession(arg0 context.Context, arg1 db.CreateSessionParams) (db.Session, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateSession", arg0, arg1)
ret0, _ := ret[0].(db.Session)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateSession indicates an expected call of CreateSession.
func (mr *MockStoreMockRecorder) CreateSession(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSession", reflect.TypeOf((*MockStore)(nil).CreateSession), arg0, arg1)
}
// DeleteAccount mocks base method. // DeleteAccount mocks base method.
func (m *MockStore) DeleteAccount(arg0 context.Context, arg1 int64) error { func (m *MockStore) DeleteAccount(arg0 context.Context, arg1 int64) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -316,6 +332,21 @@ func (mr *MockStoreMockRecorder) GetAccount(arg0, arg1 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockStore)(nil).GetAccount), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockStore)(nil).GetAccount), arg0, arg1)
} }
// GetAccountByEmail mocks base method.
func (m *MockStore) GetAccountByEmail(arg0 context.Context, arg1 string) (db.Account, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccountByEmail", arg0, arg1)
ret0, _ := ret[0].(db.Account)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccountByEmail indicates an expected call of GetAccountByEmail.
func (mr *MockStoreMockRecorder) GetAccountByEmail(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountByEmail", reflect.TypeOf((*MockStore)(nil).GetAccountByEmail), arg0, arg1)
}
// GetAccountForUpdate mocks base method. // GetAccountForUpdate mocks base method.
func (m *MockStore) GetAccountForUpdate(arg0 context.Context, arg1 int64) (db.Account, error) { func (m *MockStore) GetAccountForUpdate(arg0 context.Context, arg1 int64) (db.Account, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -436,6 +467,21 @@ func (mr *MockStoreMockRecorder) GetReturnsLog(arg0, arg1 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReturnsLog", reflect.TypeOf((*MockStore)(nil).GetReturnsLog), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReturnsLog", reflect.TypeOf((*MockStore)(nil).GetReturnsLog), arg0, arg1)
} }
// GetSession mocks base method.
func (m *MockStore) GetSession(arg0 context.Context, arg1 uuid.UUID) (db.Session, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSession", arg0, arg1)
ret0, _ := ret[0].(db.Session)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetSession indicates an expected call of GetSession.
func (mr *MockStoreMockRecorder) GetSession(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSession", reflect.TypeOf((*MockStore)(nil).GetSession), arg0, arg1)
}
// InvalidateDocument mocks base method. // InvalidateDocument mocks base method.
func (m *MockStore) InvalidateDocument(arg0 context.Context, arg1 db.InvalidateDocumentParams) (db.Document, error) { func (m *MockStore) InvalidateDocument(arg0 context.Context, arg1 db.InvalidateDocumentParams) (db.Document, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -2,6 +2,10 @@
SELECT * FROM accounts SELECT * FROM accounts
WHERE "id" = $1 LIMIT 1; WHERE "id" = $1 LIMIT 1;
-- name: GetAccountByEmail :one
SELECT * FROM accounts
WHERE "email" = $1 LIMIT 1;
-- name: GetAccountForUpdate :one -- name: GetAccountForUpdate :one
SELECT * FROM accounts SELECT * FROM accounts
WHERE "id" = $1 LIMIT 1 WHERE "id" = $1 LIMIT 1

View File

@ -7,8 +7,8 @@ INSERT INTO payments (
"account_id", "account_id",
"payment_category", "payment_category",
"bankname", "bankname",
"iban", "IBAN",
"bic", "BIC",
"paypal_account", "paypal_account",
"paypal_id", "paypal_id",
"payment_system", "payment_system",
@ -31,8 +31,8 @@ SET
"account_id" = COALESCE(sqlc.narg(account_id), "account_id"), "account_id" = COALESCE(sqlc.narg(account_id), "account_id"),
"payment_category" = COALESCE(sqlc.narg(payment_category), "payment_category"), "payment_category" = COALESCE(sqlc.narg(payment_category), "payment_category"),
"bankname" = COALESCE(sqlc.narg(bankname), "bankname"), "bankname" = COALESCE(sqlc.narg(bankname), "bankname"),
"iban" = COALESCE(sqlc.narg(iban), "iban"), "IBAN" = COALESCE(sqlc.narg(IBAN), "IBAN"),
"bic" = COALESCE(sqlc.narg(bic), "bic"), "BIC" = COALESCE(sqlc.narg(BIC), "BIC"),
"paypal_account" = COALESCE(sqlc.narg(paypal_account), "paypal_account"), "paypal_account" = COALESCE(sqlc.narg(paypal_account), "paypal_account"),
"paypal_id" = COALESCE(sqlc.narg(paypal_id), "paypal_id"), "paypal_id" = COALESCE(sqlc.narg(paypal_id), "paypal_id"),
"payment_system" = COALESCE(sqlc.narg(payment_system), "payment_system"), "payment_system" = COALESCE(sqlc.narg(payment_system), "payment_system"),

16
db/query/session.sql Normal file
View File

@ -0,0 +1,16 @@
-- name: CreateSession :one
INSERT INTO sessions (
id,
email,
refresh_token,
user_agent,
client_ip,
is_blocked,
expires_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7
) RETURNING *;
-- name: GetSession :one
SELECT * FROM sessions
WHERE id = $1 LIMIT 1;

View File

@ -42,7 +42,7 @@ INSERT INTO accounts (
$12, $12,
$13, $13,
$13 $13
) RETURNING id, passwordhash, firstname, lastname, birthday, privacy_accepted, privacy_accepted_date, email, phone, city, zip, street, country, token, token_valid, token_expiration, creator, created, changer, changed ) RETURNING id, permission_level, passwordhash, firstname, lastname, birthday, privacy_accepted, privacy_accepted_date, email, phone, city, zip, street, country, creator, created, changer, changed
` `
type CreateAccountParams struct { type CreateAccountParams struct {
@ -80,6 +80,7 @@ func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (A
var i Account var i Account
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.PermissionLevel,
&i.Passwordhash, &i.Passwordhash,
&i.Firstname, &i.Firstname,
&i.Lastname, &i.Lastname,
@ -92,9 +93,6 @@ func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (A
&i.Zip, &i.Zip,
&i.Street, &i.Street,
&i.Country, &i.Country,
&i.Token,
&i.TokenValid,
&i.TokenExpiration,
&i.Creator, &i.Creator,
&i.Created, &i.Created,
&i.Changer, &i.Changer,
@ -114,7 +112,7 @@ func (q *Queries) DeleteAccount(ctx context.Context, id int64) error {
} }
const getAccount = `-- name: GetAccount :one const getAccount = `-- name: GetAccount :one
SELECT id, passwordhash, firstname, lastname, birthday, privacy_accepted, privacy_accepted_date, email, phone, city, zip, street, country, token, token_valid, token_expiration, creator, created, changer, changed FROM accounts SELECT id, permission_level, passwordhash, firstname, lastname, birthday, privacy_accepted, privacy_accepted_date, email, phone, city, zip, street, country, creator, created, changer, changed FROM accounts
WHERE "id" = $1 LIMIT 1 WHERE "id" = $1 LIMIT 1
` `
@ -123,6 +121,38 @@ func (q *Queries) GetAccount(ctx context.Context, id int64) (Account, error) {
var i Account var i Account
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.PermissionLevel,
&i.Passwordhash,
&i.Firstname,
&i.Lastname,
&i.Birthday,
&i.PrivacyAccepted,
&i.PrivacyAcceptedDate,
&i.Email,
&i.Phone,
&i.City,
&i.Zip,
&i.Street,
&i.Country,
&i.Creator,
&i.Created,
&i.Changer,
&i.Changed,
)
return i, err
}
const getAccountByEmail = `-- name: GetAccountByEmail :one
SELECT id, permission_level, passwordhash, firstname, lastname, birthday, privacy_accepted, privacy_accepted_date, email, phone, city, zip, street, country, creator, created, changer, changed FROM accounts
WHERE "email" = $1 LIMIT 1
`
func (q *Queries) GetAccountByEmail(ctx context.Context, email string) (Account, error) {
row := q.db.QueryRowContext(ctx, getAccountByEmail, email)
var i Account
err := row.Scan(
&i.ID,
&i.PermissionLevel,
&i.Passwordhash, &i.Passwordhash,
&i.Firstname, &i.Firstname,
&i.Lastname, &i.Lastname,
@ -135,9 +165,6 @@ func (q *Queries) GetAccount(ctx context.Context, id int64) (Account, error) {
&i.Zip, &i.Zip,
&i.Street, &i.Street,
&i.Country, &i.Country,
&i.Token,
&i.TokenValid,
&i.TokenExpiration,
&i.Creator, &i.Creator,
&i.Created, &i.Created,
&i.Changer, &i.Changer,
@ -147,7 +174,7 @@ func (q *Queries) GetAccount(ctx context.Context, id int64) (Account, error) {
} }
const getAccountForUpdate = `-- name: GetAccountForUpdate :one const getAccountForUpdate = `-- name: GetAccountForUpdate :one
SELECT id, passwordhash, firstname, lastname, birthday, privacy_accepted, privacy_accepted_date, email, phone, city, zip, street, country, token, token_valid, token_expiration, creator, created, changer, changed FROM accounts SELECT id, permission_level, passwordhash, firstname, lastname, birthday, privacy_accepted, privacy_accepted_date, email, phone, city, zip, street, country, creator, created, changer, changed FROM accounts
WHERE "id" = $1 LIMIT 1 WHERE "id" = $1 LIMIT 1
FOR NO KEY UPDATE FOR NO KEY UPDATE
` `
@ -157,6 +184,7 @@ func (q *Queries) GetAccountForUpdate(ctx context.Context, id int64) (Account, e
var i Account var i Account
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.PermissionLevel,
&i.Passwordhash, &i.Passwordhash,
&i.Firstname, &i.Firstname,
&i.Lastname, &i.Lastname,
@ -169,9 +197,6 @@ func (q *Queries) GetAccountForUpdate(ctx context.Context, id int64) (Account, e
&i.Zip, &i.Zip,
&i.Street, &i.Street,
&i.Country, &i.Country,
&i.Token,
&i.TokenValid,
&i.TokenExpiration,
&i.Creator, &i.Creator,
&i.Created, &i.Created,
&i.Changer, &i.Changer,
@ -181,7 +206,7 @@ func (q *Queries) GetAccountForUpdate(ctx context.Context, id int64) (Account, e
} }
const listAccounts = `-- name: ListAccounts :many const listAccounts = `-- name: ListAccounts :many
SELECT id, passwordhash, firstname, lastname, birthday, privacy_accepted, privacy_accepted_date, email, phone, city, zip, street, country, token, token_valid, token_expiration, creator, created, changer, changed FROM accounts SELECT id, permission_level, passwordhash, firstname, lastname, birthday, privacy_accepted, privacy_accepted_date, email, phone, city, zip, street, country, creator, created, changer, changed FROM accounts
ORDER BY "lastname", "firstname" ORDER BY "lastname", "firstname"
LIMIT $1 LIMIT $1
OFFSET $2 OFFSET $2
@ -203,6 +228,7 @@ func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]A
var i Account var i Account
if err := rows.Scan( if err := rows.Scan(
&i.ID, &i.ID,
&i.PermissionLevel,
&i.Passwordhash, &i.Passwordhash,
&i.Firstname, &i.Firstname,
&i.Lastname, &i.Lastname,
@ -215,9 +241,6 @@ func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]A
&i.Zip, &i.Zip,
&i.Street, &i.Street,
&i.Country, &i.Country,
&i.Token,
&i.TokenValid,
&i.TokenExpiration,
&i.Creator, &i.Creator,
&i.Created, &i.Created,
&i.Changer, &i.Changer,
@ -252,7 +275,7 @@ SET
"changer" = $2, "changer" = $2,
"changed" = now() "changed" = now()
WHERE "id" = $1 WHERE "id" = $1
RETURNING id, passwordhash, firstname, lastname, birthday, privacy_accepted, privacy_accepted_date, email, phone, city, zip, street, country, token, token_valid, token_expiration, creator, created, changer, changed RETURNING id, permission_level, passwordhash, firstname, lastname, birthday, privacy_accepted, privacy_accepted_date, email, phone, city, zip, street, country, creator, created, changer, changed
` `
type UpdateAccountParams struct { type UpdateAccountParams struct {
@ -288,6 +311,7 @@ func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (A
var i Account var i Account
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.PermissionLevel,
&i.Passwordhash, &i.Passwordhash,
&i.Firstname, &i.Firstname,
&i.Lastname, &i.Lastname,
@ -300,9 +324,6 @@ func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (A
&i.Zip, &i.Zip,
&i.Street, &i.Street,
&i.Country, &i.Country,
&i.Token,
&i.TokenValid,
&i.TokenExpiration,
&i.Creator, &i.Creator,
&i.Created, &i.Created,
&i.Changer, &i.Changer,
@ -319,7 +340,7 @@ SET
"changer" = $3, "changer" = $3,
"changed" = now() "changed" = now()
WHERE "id" = $4 WHERE "id" = $4
RETURNING id, passwordhash, firstname, lastname, birthday, privacy_accepted, privacy_accepted_date, email, phone, city, zip, street, country, token, token_valid, token_expiration, creator, created, changer, changed RETURNING id, permission_level, passwordhash, firstname, lastname, birthday, privacy_accepted, privacy_accepted_date, email, phone, city, zip, street, country, creator, created, changer, changed
` `
type UpdateAccountPrivacyParams struct { type UpdateAccountPrivacyParams struct {
@ -339,6 +360,7 @@ func (q *Queries) UpdateAccountPrivacy(ctx context.Context, arg UpdateAccountPri
var i Account var i Account
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.PermissionLevel,
&i.Passwordhash, &i.Passwordhash,
&i.Firstname, &i.Firstname,
&i.Lastname, &i.Lastname,
@ -351,9 +373,6 @@ func (q *Queries) UpdateAccountPrivacy(ctx context.Context, arg UpdateAccountPri
&i.Zip, &i.Zip,
&i.Street, &i.Street,
&i.Country, &i.Country,
&i.Token,
&i.TokenValid,
&i.TokenExpiration,
&i.Creator, &i.Creator,
&i.Created, &i.Created,
&i.Changer, &i.Changer,

View File

@ -7,10 +7,13 @@ package db
import ( import (
"database/sql" "database/sql"
"time" "time"
"github.com/google/uuid"
) )
type Account struct { type Account struct {
ID int64 `json:"id"` ID int64 `json:"id"`
PermissionLevel int32 `json:"permission_level"`
Passwordhash string `json:"passwordhash"` Passwordhash string `json:"passwordhash"`
Firstname string `json:"firstname"` Firstname string `json:"firstname"`
Lastname string `json:"lastname"` Lastname string `json:"lastname"`
@ -23,9 +26,6 @@ type Account struct {
Zip string `json:"zip"` Zip string `json:"zip"`
Street string `json:"street"` Street string `json:"street"`
Country string `json:"country"` Country string `json:"country"`
Token sql.NullString `json:"token"`
TokenValid sql.NullBool `json:"token_valid"`
TokenExpiration time.Time `json:"token_expiration"`
Creator string `json:"creator"` Creator string `json:"creator"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
Changer string `json:"changer"` Changer string `json:"changer"`
@ -68,8 +68,8 @@ type Payment struct {
AccountID int64 `json:"account_id"` AccountID int64 `json:"account_id"`
PaymentCategory string `json:"payment_category"` PaymentCategory string `json:"payment_category"`
Bankname sql.NullString `json:"bankname"` Bankname sql.NullString `json:"bankname"`
Iban sql.NullString `json:"iban"` IBAN sql.NullString `json:"IBAN"`
Bic sql.NullString `json:"bic"` BIC sql.NullString `json:"BIC"`
PaypalAccount sql.NullString `json:"paypal_account"` PaypalAccount sql.NullString `json:"paypal_account"`
PaypalID sql.NullString `json:"paypal_id"` PaypalID sql.NullString `json:"paypal_id"`
PaymentSystem sql.NullString `json:"payment_system"` PaymentSystem sql.NullString `json:"payment_system"`
@ -133,3 +133,14 @@ type ReturnsLog struct {
Changer string `json:"changer"` Changer string `json:"changer"`
Changed time.Time `json:"changed"` Changed time.Time `json:"changed"`
} }
type Session struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
UserAgent string `json:"user_agent"`
ClientIp string `json:"client_ip"`
RefreshToken string `json:"refresh_token"`
IsBlocked bool `json:"is_blocked"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
}

View File

@ -15,8 +15,8 @@ INSERT INTO payments (
"account_id", "account_id",
"payment_category", "payment_category",
"bankname", "bankname",
"iban", "IBAN",
"bic", "BIC",
"paypal_account", "paypal_account",
"paypal_id", "paypal_id",
"payment_system", "payment_system",
@ -25,15 +25,15 @@ INSERT INTO payments (
"changer" "changer"
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
) RETURNING id, account_id, payment_category, bankname, iban, bic, paypal_account, paypal_id, payment_system, type, creator, created, changer, changed ) RETURNING id, account_id, payment_category, bankname, "IBAN", "BIC", paypal_account, paypal_id, payment_system, type, creator, created, changer, changed
` `
type CreatePaymentParams struct { type CreatePaymentParams struct {
AccountID int64 `json:"account_id"` AccountID int64 `json:"account_id"`
PaymentCategory string `json:"payment_category"` PaymentCategory string `json:"payment_category"`
Bankname sql.NullString `json:"bankname"` Bankname sql.NullString `json:"bankname"`
Iban sql.NullString `json:"iban"` IBAN sql.NullString `json:"IBAN"`
Bic sql.NullString `json:"bic"` BIC sql.NullString `json:"BIC"`
PaypalAccount sql.NullString `json:"paypal_account"` PaypalAccount sql.NullString `json:"paypal_account"`
PaypalID sql.NullString `json:"paypal_id"` PaypalID sql.NullString `json:"paypal_id"`
PaymentSystem sql.NullString `json:"payment_system"` PaymentSystem sql.NullString `json:"payment_system"`
@ -47,8 +47,8 @@ func (q *Queries) CreatePayment(ctx context.Context, arg CreatePaymentParams) (P
arg.AccountID, arg.AccountID,
arg.PaymentCategory, arg.PaymentCategory,
arg.Bankname, arg.Bankname,
arg.Iban, arg.IBAN,
arg.Bic, arg.BIC,
arg.PaypalAccount, arg.PaypalAccount,
arg.PaypalID, arg.PaypalID,
arg.PaymentSystem, arg.PaymentSystem,
@ -62,8 +62,8 @@ func (q *Queries) CreatePayment(ctx context.Context, arg CreatePaymentParams) (P
&i.AccountID, &i.AccountID,
&i.PaymentCategory, &i.PaymentCategory,
&i.Bankname, &i.Bankname,
&i.Iban, &i.IBAN,
&i.Bic, &i.BIC,
&i.PaypalAccount, &i.PaypalAccount,
&i.PaypalID, &i.PaypalID,
&i.PaymentSystem, &i.PaymentSystem,
@ -87,7 +87,7 @@ func (q *Queries) DeletePayment(ctx context.Context, id int64) error {
} }
const getPayment = `-- name: GetPayment :one const getPayment = `-- name: GetPayment :one
SELECT id, account_id, payment_category, bankname, iban, bic, paypal_account, paypal_id, payment_system, type, creator, created, changer, changed FROM payments SELECT id, account_id, payment_category, bankname, "IBAN", "BIC", paypal_account, paypal_id, payment_system, type, creator, created, changer, changed FROM payments
WHERE "id" = $1 LIMIT 1 WHERE "id" = $1 LIMIT 1
` `
@ -99,8 +99,8 @@ func (q *Queries) GetPayment(ctx context.Context, id int64) (Payment, error) {
&i.AccountID, &i.AccountID,
&i.PaymentCategory, &i.PaymentCategory,
&i.Bankname, &i.Bankname,
&i.Iban, &i.IBAN,
&i.Bic, &i.BIC,
&i.PaypalAccount, &i.PaypalAccount,
&i.PaypalID, &i.PaypalID,
&i.PaymentSystem, &i.PaymentSystem,
@ -114,7 +114,7 @@ func (q *Queries) GetPayment(ctx context.Context, id int64) (Payment, error) {
} }
const listPayments = `-- name: ListPayments :many const listPayments = `-- name: ListPayments :many
SELECT id, account_id, payment_category, bankname, iban, bic, paypal_account, paypal_id, payment_system, type, creator, created, changer, changed FROM payments SELECT id, account_id, payment_category, bankname, "IBAN", "BIC", paypal_account, paypal_id, payment_system, type, creator, created, changer, changed FROM payments
ORDER BY "payment_category" ORDER BY "payment_category"
LIMIT $1 LIMIT $1
OFFSET $2 OFFSET $2
@ -139,8 +139,8 @@ func (q *Queries) ListPayments(ctx context.Context, arg ListPaymentsParams) ([]P
&i.AccountID, &i.AccountID,
&i.PaymentCategory, &i.PaymentCategory,
&i.Bankname, &i.Bankname,
&i.Iban, &i.IBAN,
&i.Bic, &i.BIC,
&i.PaypalAccount, &i.PaypalAccount,
&i.PaypalID, &i.PaypalID,
&i.PaymentSystem, &i.PaymentSystem,
@ -169,8 +169,8 @@ SET
"account_id" = COALESCE($3, "account_id"), "account_id" = COALESCE($3, "account_id"),
"payment_category" = COALESCE($4, "payment_category"), "payment_category" = COALESCE($4, "payment_category"),
"bankname" = COALESCE($5, "bankname"), "bankname" = COALESCE($5, "bankname"),
"iban" = COALESCE($6, "iban"), "IBAN" = COALESCE($6, "IBAN"),
"bic" = COALESCE($7, "bic"), "BIC" = COALESCE($7, "BIC"),
"paypal_account" = COALESCE($8, "paypal_account"), "paypal_account" = COALESCE($8, "paypal_account"),
"paypal_id" = COALESCE($9, "paypal_id"), "paypal_id" = COALESCE($9, "paypal_id"),
"payment_system" = COALESCE($10, "payment_system"), "payment_system" = COALESCE($10, "payment_system"),
@ -178,7 +178,7 @@ SET
"changer" = $2, "changer" = $2,
"changed" = now() "changed" = now()
WHERE "id" = $1 WHERE "id" = $1
RETURNING id, account_id, payment_category, bankname, iban, bic, paypal_account, paypal_id, payment_system, type, creator, created, changer, changed RETURNING id, account_id, payment_category, bankname, "IBAN", "BIC", paypal_account, paypal_id, payment_system, type, creator, created, changer, changed
` `
type UpdatePaymentParams struct { type UpdatePaymentParams struct {
@ -215,8 +215,8 @@ func (q *Queries) UpdatePayment(ctx context.Context, arg UpdatePaymentParams) (P
&i.AccountID, &i.AccountID,
&i.PaymentCategory, &i.PaymentCategory,
&i.Bankname, &i.Bankname,
&i.Iban, &i.IBAN,
&i.Bic, &i.BIC,
&i.PaypalAccount, &i.PaypalAccount,
&i.PaypalID, &i.PaypalID,
&i.PaymentSystem, &i.PaymentSystem,

View File

@ -23,11 +23,11 @@ func createRandomPayment(t *testing.T) Payment {
Valid: true, Valid: true,
String: util.RandomName(), String: util.RandomName(),
}, },
Iban: sql.NullString{ IBAN: sql.NullString{
Valid: true, Valid: true,
String: util.RandomName(), String: util.RandomName(),
}, },
Bic: sql.NullString{ BIC: sql.NullString{
Valid: true, Valid: true,
String: util.RandomName(), String: util.RandomName(),
}, },
@ -55,8 +55,8 @@ func createRandomPayment(t *testing.T) Payment {
require.Equal(t, arg.PaymentCategory, person.PaymentCategory) require.Equal(t, arg.PaymentCategory, person.PaymentCategory)
require.Equal(t, arg.Bankname, person.Bankname) require.Equal(t, arg.Bankname, person.Bankname)
require.Equal(t, arg.AccountID, person.AccountID) require.Equal(t, arg.AccountID, person.AccountID)
require.Equal(t, arg.Iban, person.Iban) require.Equal(t, arg.IBAN, person.IBAN)
require.Equal(t, arg.Bic, person.Bic) require.Equal(t, arg.BIC, person.BIC)
require.Equal(t, arg.PaypalAccount, person.PaypalAccount) require.Equal(t, arg.PaypalAccount, person.PaypalAccount)
require.Equal(t, arg.PaymentSystem, person.PaymentSystem) require.Equal(t, arg.PaymentSystem, person.PaymentSystem)
require.Equal(t, arg.PaypalID, person.PaypalID) require.Equal(t, arg.PaypalID, person.PaypalID)
@ -84,8 +84,8 @@ func TestGetPayment(t *testing.T) {
require.Equal(t, newperson.PaymentCategory, person.PaymentCategory) require.Equal(t, newperson.PaymentCategory, person.PaymentCategory)
require.Equal(t, newperson.Bankname, person.Bankname) require.Equal(t, newperson.Bankname, person.Bankname)
require.Equal(t, newperson.AccountID, person.AccountID) require.Equal(t, newperson.AccountID, person.AccountID)
require.Equal(t, newperson.Iban, person.Iban) require.Equal(t, newperson.IBAN, person.IBAN)
require.Equal(t, newperson.Bic, person.Bic) require.Equal(t, newperson.BIC, person.BIC)
require.Equal(t, newperson.PaypalAccount, person.PaypalAccount) require.Equal(t, newperson.PaypalAccount, person.PaypalAccount)
require.Equal(t, newperson.PaymentSystem, person.PaymentSystem) require.Equal(t, newperson.PaymentSystem, person.PaymentSystem)
require.Equal(t, newperson.PaypalID, person.PaypalID) require.Equal(t, newperson.PaypalID, person.PaypalID)

View File

@ -6,6 +6,8 @@ package db
import ( import (
"context" "context"
"github.com/google/uuid"
) )
type Querier interface { type Querier interface {
@ -18,6 +20,7 @@ type Querier interface {
CreateProvider(ctx context.Context, arg CreateProviderParams) (Provider, error) CreateProvider(ctx context.Context, arg CreateProviderParams) (Provider, error)
CreateReturn(ctx context.Context, arg CreateReturnParams) (Return, error) CreateReturn(ctx context.Context, arg CreateReturnParams) (Return, error)
CreateReturnsLog(ctx context.Context, arg CreateReturnsLogParams) (ReturnsLog, error) CreateReturnsLog(ctx context.Context, arg CreateReturnsLogParams) (ReturnsLog, error)
CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
DeleteAccount(ctx context.Context, id int64) error DeleteAccount(ctx context.Context, id int64) error
DeleteDocument(ctx context.Context, id int64) error DeleteDocument(ctx context.Context, id int64) error
// -- name: UpdateMail :one // -- name: UpdateMail :one
@ -40,6 +43,7 @@ type Querier interface {
DeleteReturn(ctx context.Context, id int64) error DeleteReturn(ctx context.Context, id int64) error
DeleteReturnsLog(ctx context.Context, id int64) error DeleteReturnsLog(ctx context.Context, id int64) error
GetAccount(ctx context.Context, id int64) (Account, error) GetAccount(ctx context.Context, id int64) (Account, error)
GetAccountByEmail(ctx context.Context, email string) (Account, error)
GetAccountForUpdate(ctx context.Context, id int64) (Account, error) GetAccountForUpdate(ctx context.Context, id int64) (Account, error)
GetDocument(ctx context.Context, id int64) (Document, error) GetDocument(ctx context.Context, id int64) (Document, error)
GetMail(ctx context.Context, id int64) (Mail, error) GetMail(ctx context.Context, id int64) (Mail, error)
@ -48,6 +52,7 @@ type Querier interface {
GetProvider(ctx context.Context, id int64) (Provider, error) GetProvider(ctx context.Context, id int64) (Provider, error)
GetReturn(ctx context.Context, id int64) (Return, error) GetReturn(ctx context.Context, id int64) (Return, error)
GetReturnsLog(ctx context.Context, id int64) (ReturnsLog, error) GetReturnsLog(ctx context.Context, id int64) (ReturnsLog, error)
GetSession(ctx context.Context, id uuid.UUID) (Session, error)
InvalidateDocument(ctx context.Context, arg InvalidateDocumentParams) (Document, error) InvalidateDocument(ctx context.Context, arg InvalidateDocumentParams) (Document, error)
ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error)
ListDocuments(ctx context.Context, arg ListDocumentsParams) ([]Document, error) ListDocuments(ctx context.Context, arg ListDocumentsParams) ([]Document, error)

82
db/sqlc/session.sql.go Normal file
View File

@ -0,0 +1,82 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.21.0
// source: session.sql
package db
import (
"context"
"time"
"github.com/google/uuid"
)
const createSession = `-- name: CreateSession :one
INSERT INTO sessions (
id,
email,
refresh_token,
user_agent,
client_ip,
is_blocked,
expires_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7
) RETURNING id, email, user_agent, client_ip, refresh_token, is_blocked, expires_at, created_at
`
type CreateSessionParams struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
RefreshToken string `json:"refresh_token"`
UserAgent string `json:"user_agent"`
ClientIp string `json:"client_ip"`
IsBlocked bool `json:"is_blocked"`
ExpiresAt time.Time `json:"expires_at"`
}
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
row := q.db.QueryRowContext(ctx, createSession,
arg.ID,
arg.Email,
arg.RefreshToken,
arg.UserAgent,
arg.ClientIp,
arg.IsBlocked,
arg.ExpiresAt,
)
var i Session
err := row.Scan(
&i.ID,
&i.Email,
&i.UserAgent,
&i.ClientIp,
&i.RefreshToken,
&i.IsBlocked,
&i.ExpiresAt,
&i.CreatedAt,
)
return i, err
}
const getSession = `-- name: GetSession :one
SELECT id, email, user_agent, client_ip, refresh_token, is_blocked, expires_at, created_at FROM sessions
WHERE id = $1 LIMIT 1
`
func (q *Queries) GetSession(ctx context.Context, id uuid.UUID) (Session, error) {
row := q.db.QueryRowContext(ctx, getSession, id)
var i Session
err := row.Scan(
&i.ID,
&i.Email,
&i.UserAgent,
&i.ClientIp,
&i.RefreshToken,
&i.IsBlocked,
&i.ExpiresAt,
&i.CreatedAt,
)
return i, err
}

View File

@ -4,6 +4,8 @@ import (
"context" "context"
"database/sql" "database/sql"
"time" "time"
"github.com/itsscb/df/util"
) )
type CreateAccountTxParams struct { type CreateAccountTxParams struct {
@ -28,6 +30,7 @@ type CreateAccountTxResult struct {
func (store *SQLStore) CreateAccountTx(ctx context.Context, arg CreateAccountTxParams) (Account, error) { func (store *SQLStore) CreateAccountTx(ctx context.Context, arg CreateAccountTxParams) (Account, error) {
var result CreateAccountTxResult var result CreateAccountTxResult
var err error
if arg.PrivacyAccepted.Bool && arg.PrivacyAccepted.Valid && !arg.PrivacyAcceptedDate.Valid { if arg.PrivacyAccepted.Bool && arg.PrivacyAccepted.Valid && !arg.PrivacyAcceptedDate.Valid {
arg.PrivacyAcceptedDate = sql.NullTime{ arg.PrivacyAcceptedDate = sql.NullTime{
@ -36,7 +39,12 @@ func (store *SQLStore) CreateAccountTx(ctx context.Context, arg CreateAccountTxP
} }
} }
err := store.execTx(ctx, func(q *Queries) error { arg.Passwordhash, err = util.HashPassword(arg.Passwordhash)
if err != nil {
return Account{}, nil
}
err = store.execTx(ctx, func(q *Queries) error {
var err error var err error
result.Account, err = q.CreateAccount(ctx, CreateAccountParams(arg)) result.Account, err = q.CreateAccount(ctx, CreateAccountParams(arg))

View File

@ -3,6 +3,8 @@ package db
import ( import (
"context" "context"
"database/sql" "database/sql"
"github.com/itsscb/df/util"
) )
type UpdateAccountTxParams struct { type UpdateAccountTxParams struct {
@ -26,8 +28,16 @@ type UpdateAccountTxResult struct {
func (store *SQLStore) UpdateAccountTx(ctx context.Context, arg UpdateAccountTxParams) (Account, error) { func (store *SQLStore) UpdateAccountTx(ctx context.Context, arg UpdateAccountTxParams) (Account, error) {
var result UpdateAccountTxResult var result UpdateAccountTxResult
var err error
err := store.execTx(ctx, func(q *Queries) error { if arg.Passwordhash.Valid {
arg.Passwordhash.String, err = util.HashPassword(arg.Passwordhash.String)
if err != nil {
return Account{}, nil
}
}
err = store.execTx(ctx, func(q *Queries) error {
var err error var err error
result.Account, err = q.UpdateAccount(ctx, UpdateAccountParams(arg)) result.Account, err = q.UpdateAccount(ctx, UpdateAccountParams(arg))
return err return err

9
go.mod
View File

@ -5,14 +5,21 @@ go 1.21
toolchain go1.21.1 toolchain go1.21.1
require ( require (
github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.1.2
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/o1egl/paseto v1.0.0
github.com/spf13/viper v1.16.0 github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
go.uber.org/mock v0.3.0 go.uber.org/mock v0.3.0
golang.org/x/crypto v0.13.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
) )
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.10.1 // indirect github.com/bytedance/sonic v1.10.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect github.com/chenzhuoyu/iasm v0.9.0 // indirect
@ -34,6 +41,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/afero v1.9.5 // indirect github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect github.com/spf13/cast v1.5.1 // indirect
@ -43,7 +51,6 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.5.0 // indirect golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/net v0.15.0 // indirect golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect

16
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= 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/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/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.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc= github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc=
@ -143,6 +150,7 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@ -185,8 +193,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/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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
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/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -240,6 +252,7 @@ go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.5.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-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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -259,6 +272,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -336,6 +351,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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-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-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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -21,7 +21,10 @@ func main() {
} }
store := db.NewStore(conn) store := db.NewStore(conn)
server := api.NewServer(config, store) server, err := api.NewServer(config, store)
if err != nil {
log.Fatalf("could not start server: %s", err)
}
err = server.Start(config.ServerAddress) err = server.Start(config.ServerAddress)
if err != nil { if err != nil {

14
token/maker.go Normal file
View File

@ -0,0 +1,14 @@
package token
import (
"time"
)
// Maker is an interface for managing tokens
type Maker interface {
// CreateToken creates a new token for a specific username and duration
CreateToken(email string, duration time.Duration) (string, *Payload, error)
// VerifyToken checks if the token is valid or not
VerifyToken(token string) (*Payload, error)
}

57
token/paseto_maker.go Normal file
View File

@ -0,0 +1,57 @@
package token
import (
"fmt"
"time"
"github.com/aead/chacha20poly1305"
"github.com/o1egl/paseto"
)
// PasetoMaker is a PASETO token maker
type PasetoMaker struct {
paseto *paseto.V2
symmetricKey []byte
}
// NewPasetoMaker creates a new PasetoMaker
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
}
// CreateToken creates a new token for a specific username and duration
func (maker *PasetoMaker) CreateToken(email string, duration time.Duration) (string, *Payload, error) {
payload, err := NewPayload(email, duration)
if err != nil {
return "", payload, err
}
token, err := maker.paseto.Encrypt(maker.symmetricKey, payload, nil)
return token, payload, err
}
// VerifyToken checks if the token is valid or not
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,49 @@
package token
import (
"testing"
"time"
"github.com/itsscb/df/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, payload, err := maker.CreateToken(email, duration)
require.NoError(t, err)
require.NotEmpty(t, token)
require.NotEmpty(t, payload)
payload, err = maker.VerifyToken(token)
require.NoError(t, err)
require.NotEmpty(t, token)
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, payload, err := maker.CreateToken(util.RandomEmail(), -time.Minute)
require.NoError(t, err)
require.NotEmpty(t, token)
require.NotEmpty(t, payload)
payload, err = maker.VerifyToken(token)
require.Error(t, err)
require.EqualError(t, err, ErrExpiredToken.Error())
require.Nil(t, payload)
}

46
token/payload.go Normal file
View File

@ -0,0 +1,46 @@
package token
import (
"errors"
"time"
"github.com/google/uuid"
)
// Different types of error returned by the VerifyToken function
var (
ErrInvalidToken = errors.New("token is invalid")
ErrExpiredToken = errors.New("token has expired")
)
// Payload contains the payload data of the token
type Payload struct {
ID uuid.UUID `json:"id"`
Email string `json:"account_id"`
IssuedAt time.Time `json:"issued_at"`
ExpiredAt time.Time `json:"expired_at"`
}
// NewPayload creates a new token payload with a specific accountID and duration
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
}
// Valid checks if the token payload is valid or not
func (payload *Payload) Valid() error {
if time.Now().After(payload.ExpiredAt) {
return ErrExpiredToken
}
return nil
}

View File

@ -1,13 +1,20 @@
package util package util
import "github.com/spf13/viper" import (
"time"
"github.com/spf13/viper"
)
type Config struct { type Config struct {
DBSource string `mapstructure:"DB_SOURCE"` DBSource string `mapstructure:"DB_SOURCE"`
DBDriver string `mapstructure:"DB_DRIVER"` DBDriver string `mapstructure:"DB_DRIVER"`
ServerAddress string `mapstructure:"SERVER_ADDRESS"` ServerAddress string `mapstructure:"SERVER_ADDRESS"`
Environment string `mapstructure:"ENVIRONMENT"` Environment string `mapstructure:"ENVIRONMENT"`
LogOutput string `mapstructure:"LOG_OUTPUT"` LogOutput string `mapstructure:"LOG_OUTPUT"`
TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"`
AccessTokenDuration time.Duration `mapstructure:"ACCESS_TOKEN_DURATION"`
RefreshTokenDuration time.Duration `mapstructure:"REFRESH_TOKEN_DURATION"`
} }
func LoadConfig(path string) (config Config, err error) { func LoadConfig(path string) (config Config, err error) {

21
util/password.go Normal file
View File

@ -0,0 +1,21 @@
package util
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
// HashPassword returns the bcrypt hash of the password
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
}
// CheckPassword checks if the provided password is correct or not
func CheckPassword(password string, hashedPassword string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}

28
util/password_test.go Normal file
View File

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