Compare commits

...

10 Commits

Author SHA1 Message Date
94d04b83d9 chore: migrate to gitea
Some checks failed
golangci-lint / lint (push) Failing after 5s
Test / test (push) Failing after 4s
2026-01-27 00:19:33 +01:00
f100a1a46a wip 2023-12-01 22:11:49 +01:00
932f423faf big refacto 2023-10-03 00:40:01 +02:00
aa722718f7 wip 2022-10-20 10:39:56 +02:00
c37d824191 add b asic endpoint 2022-05-20 00:52:07 +02:00
19a642b4d4 debut ajout compte joint 2022-05-13 01:38:03 +02:00
cc6aa27d5d feat(expense): import expense 2021-11-30 01:46:51 +01:00
53b0b8c9a2 feat(expense): create and display expenses 2021-11-26 01:51:13 +01:00
82d86fb33f feat(expense): starting expense handle 2021-11-24 01:07:19 +01:00
917c3a4318 refactor: now validate request with middleware 2021-11-24 00:53:56 +01:00
38 changed files with 934 additions and 457 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -0,0 +1,30 @@
name: golangci-lint
on:
push:
pull_request:
permissions:
contents: read
# Optional: allow read access to pull requests. Use with `only-new-issues` option.
# pull-requests: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # all history for all branches and tags
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.3.1

27
.gitea/workflows/test.yml Normal file
View File

@@ -0,0 +1,27 @@
name: Test
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # all history for all branches and tags
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
cache: false
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y make
- name: Run make check-all
run: make test

View File

@@ -43,18 +43,13 @@ local-format: ## format locally all files
gofmt -s -l -w . gofmt -s -l -w .
.PHONY: build .PHONY: build
build: sources-image ## Build the docker image with application binary build: ## Build the docker image with application binary
@echo "+ $@" @echo "+ $@"
docker build --no-cache \ docker build --no-cache \
-f containers/Dockerfile \ -f containers/Dockerfile \
--build-arg SOURCES_IMAGE=$(NAME)-sources:$(VERSION) \
-t poketools:$(VERSION) . -t poketools:$(VERSION) .
GIT_CREDENTIALS?=$(shell cat ~/.git-credentials 2> /dev/null) GIT_CREDENTIALS?=$(shell cat ~/.git-credentials 2> /dev/null)
.PHONY: sources-image
sources-image: ## Generate a Docker image with only the sources
@echo "+ $@"
docker build -t $(NAME)-sources:$(VERSION) -f containers/Dockerfile.sources .
.PHONY: local-run-dependencies .PHONY: local-run-dependencies

View File

@@ -2,9 +2,14 @@ package cmd
import ( import (
"fmt" "fmt"
"nos-comptes/ginserver"
"nos-comptes/handler" "nos-comptes/handler"
"nos-comptes/internal/account"
"nos-comptes/internal/expense"
ginserver "nos-comptes/internal/ginserver"
"nos-comptes/internal/storage/dao/postgresql"
"nos-comptes/internal/user"
"nos-comptes/internal/utils" "nos-comptes/internal/utils"
validatorInternal "nos-comptes/internal/utils/validator"
"os" "os"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@@ -20,7 +25,6 @@ var (
const ( const (
parameterConfigurationFile = "config" parameterConfigurationFile = "config"
parameterLogLevel = "loglevel" parameterLogLevel = "loglevel"
parameterMock = "mock"
parameterLogFormat = "logformat" parameterLogFormat = "logformat"
parameterDBConnectionURI = "dbconnectionuri" parameterDBConnectionURI = "dbconnectionuri"
parameterPort = "port" parameterPort = "port"
@@ -40,15 +44,19 @@ var rootCmd = &cobra.Command{
utils.InitLogger(config.LogLevel, config.LogFormat) utils.InitLogger(config.LogLevel, config.LogFormat)
logrus. logrus.
WithField(parameterConfigurationFile, cfgFile). WithField(parameterConfigurationFile, cfgFile).
WithField(parameterMock, config.Mock).
WithField(parameterLogLevel, config.LogLevel). WithField(parameterLogLevel, config.LogLevel).
WithField(parameterLogFormat, config.LogFormat). WithField(parameterLogFormat, config.LogFormat).
WithField(parameterPort, config.Port). WithField(parameterPort, config.Port).
WithField(parameterDBConnectionURI, config.DBConnectionURI). WithField(parameterDBConnectionURI, config.DBConnectionURI).
Warn("Configuration") Warn("Configuration")
injector := &utils.Injector{}
router := ginserver.NewRouter(config) ginserver.Setup(injector, config)
router.Run(fmt.Sprintf(":%d", config.Port)) postgresql.Setup(injector, config.DBConnectionURI)
validatorInternal.Setup(injector)
user.Setup(injector)
account.Setup(injector)
expense.Setup(injector)
ginserver.Start(injector)
}, },
} }
@@ -76,8 +84,6 @@ func init() {
rootCmd.Flags().Int(parameterPort, defaultPort, "Use this flag to set the listening port of the api") rootCmd.Flags().Int(parameterPort, defaultPort, "Use this flag to set the listening port of the api")
viper.BindPFlag(parameterPort, rootCmd.Flags().Lookup(parameterPort)) viper.BindPFlag(parameterPort, rootCmd.Flags().Lookup(parameterPort))
rootCmd.Flags().Bool(parameterMock, false, "Use this flag to enable the mock mode")
viper.BindPFlag(parameterMock, rootCmd.Flags().Lookup(parameterMock))
} }
// initConfig reads in config file and ENV variables if set. // initConfig reads in config file and ENV variables if set.
@@ -96,7 +102,6 @@ func initConfig() {
config.LogLevel = viper.GetString(parameterLogLevel) config.LogLevel = viper.GetString(parameterLogLevel)
config.LogFormat = viper.GetString(parameterLogFormat) config.LogFormat = viper.GetString(parameterLogFormat)
config.Mock = viper.GetBool(parameterMock)
config.DBConnectionURI = viper.GetString(parameterDBConnectionURI) config.DBConnectionURI = viper.GetString(parameterDBConnectionURI)
config.Port = viper.GetInt(parameterPort) config.Port = viper.GetInt(parameterPort)
} }

View File

@@ -1,73 +0,0 @@
package ginserver
import (
"net/http"
"nos-comptes/handler"
"nos-comptes/internal/account"
"nos-comptes/internal/expense"
sharedaccount "nos-comptes/internal/shared-account"
"nos-comptes/internal/storage/dao/postgresql"
"nos-comptes/internal/user"
"nos-comptes/middleware"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-contrib/cors"
)
func NewRouter(config *handler.Config) *gin.Engine {
gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.HandleMethodNotAllowed = true
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:8080/", "http://localhost:8080"},
AllowMethods: []string{"*"},
AllowHeaders: []string{"*"},
ExposeHeaders: []string{"*"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
router.Use(gin.Recovery())
router.Use(GetLoggerMiddleware())
router.Use(GetHTTPLoggerMiddleware())
db := postgresql.NewDatabasePostgreSQL(config.DBConnectionURI)
hc := handler.NewContext()
uh := user.NewHandler(hc, db)
ah := account.NewHandler(hc, db)
sah := sharedaccount.NewHandler(hc, db)
eh := expense.NewHandler(hc, db)
public := router.Group("/")
public.Handle(http.MethodGet, "/_health", hc.GetHealth)
userRoute := public.Group("/users")
userRoute.Handle("GET", "", uh.ConnectUser)
userRoute.Handle(http.MethodPost, "", uh.CreateUser)
securedUserRoute := userRoute.Group("")
securedUserRoute.Use(middleware.ValidateOAuthToken)
//TODO add secure auth
securedUserRoute.Handle(http.MethodGet, "/:userId", uh.GetUser)
//account route
securedUserRoute.Handle(http.MethodGet, "/:userId/accounts", ah.GetAllAccountOfUser)
securedUserRoute.Handle(http.MethodPost, "/:userId/accounts", ah.CreateAccountOfUser)
securedUserRoute.Handle(http.MethodDelete, "/:userId/accounts/:accountId", ah.DeleteAccountOfUser)
securedUserRoute.Handle(http.MethodGet, "/:userId/accounts/:accountId", ah.GetSpecificAccountOfUser)
//shared route
securedUserRoute.Handle(http.MethodPost, "/:userId/sharedaccounts/:accountId", sah.ShareAnAccount)
securedUserRoute.Handle(http.MethodDelete, "/:userId/sharedaccounts/:accountId", sah.DeleteSharedAccount)
securedUserRoute.Handle(http.MethodGet, "/:userId/sharedaccounts", sah.GetAllSharedAccountOfUser)
securedUserRoute.Handle(http.MethodGet, "/:userId/sharedaccounts/:sharedAccountId", sah.GetSpecificSharedAccountOfUser)
securedUserRoute.Handle(http.MethodPost, "/:userId/accounts/:accountId/expenses", eh.CreateAnExpense)
securedUserRoute.Handle(http.MethodDelete, "/:userId/accounts/:accountId/expenses/:expenseId", eh.DeleteExpense)
securedUserRoute.Handle(http.MethodGet, "/:userId/accounts/:accountId/expenses", eh.GetAllExpenses)
securedUserRoute.Handle(http.MethodGet, "/:userId/accounts/:accountId/expenses/:expenseId", eh.GetAnExpenses)
return router
}

3
go.mod
View File

@@ -1,6 +1,6 @@
module nos-comptes module nos-comptes
go 1.17 go 1.18
require ( require (
github.com/gin-contrib/cors v1.3.1 github.com/gin-contrib/cors v1.3.1
@@ -20,6 +20,7 @@ require (
github.com/go-playground/locales v0.13.0 // indirect github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/gocarina/gocsv v0.0.0-20211020200912-82fc2684cc48 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect github.com/googleapis/gax-go/v2 v2.1.1 // indirect

2
go.sum
View File

@@ -142,6 +142,8 @@ github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gocarina/gocsv v0.0.0-20211020200912-82fc2684cc48 h1:hLeicZW4XBuaISuJPfjkprg0SP0xxsQmb31aJZ6lnIw=
github.com/gocarina/gocsv v0.0.0-20211020200912-82fc2684cc48/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=

View File

@@ -1,51 +1,18 @@
package handler package handler
import ( import (
"net/http"
"nos-comptes/internal/storage/validators"
"nos-comptes/internal/utils"
"reflect"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gopkg.in/go-playground/validator.v9" "net/http"
"nos-comptes/internal/utils"
) )
type Config struct { type Config struct {
Mock bool
DBConnectionURI string DBConnectionURI string
Port int Port int
LogLevel string LogLevel string
LogFormat string LogFormat string
} }
type Context struct { func GetHealth(c *gin.Context) {
Validator *validator.Validate
}
func NewContext() *Context {
return &Context{Validator: newValidator()}
}
func newValidator() *validator.Validate {
va := validator.New()
va.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)
if len(name) < 1 {
return ""
}
return name[0]
})
for k, v := range validators.CustomValidators {
if v.Validator != nil {
va.RegisterValidationCtx(k, v.Validator)
}
}
return va
}
func (hc *Context) GetHealth(c *gin.Context) {
utils.JSON(c.Writer, http.StatusNoContent, nil) utils.JSON(c.Writer, http.StatusNoContent, nil)
} }

BIN
internal/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -2,13 +2,10 @@ package account
import ( import (
"net/http" "net/http"
"nos-comptes/handler"
"nos-comptes/internal/storage/dao/postgresql"
"nos-comptes/internal/storage/model" "nos-comptes/internal/storage/model"
"nos-comptes/internal/storage/validators" "nos-comptes/internal/storage/validators"
"nos-comptes/internal/user" "nos-comptes/internal/user"
"nos-comptes/internal/utils" "nos-comptes/internal/utils"
utils2 "nos-comptes/internal/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -17,27 +14,11 @@ type Context struct {
service *Service service *Service
db *Database db *Database
userService *user.Service userService *user.Service
*handler.Context validator *Validator
} }
func (c *Context) GetAllAccountOfUser(gc *gin.Context) { func (c *Context) GetAllAccountOfUser(gc *gin.Context) {
userId := gc.Param("userId") userId := gc.Param("userId")
err := c.Validator.VarCtx(gc, userId, "uuid4")
if err != nil {
utils2.JSONError(gc.Writer, validators.NewDataValidationAPIError(err))
return
}
_, err = c.userService.GetUserById(userId)
if e, ok := err.(*model.APIError); ok {
utils.GetLoggerFromCtx(gc).WithError(err).WithField("type", e.Type).Error("error GetUser: get user error")
utils.JSONErrorWithMessage(gc.Writer, *e, e.Description)
} else if err != nil {
utils.GetLoggerFromCtx(gc).WithError(err).Error("error while get user")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
}
accounts, err := c.service.GetAllAccountOfUser(userId) accounts, err := c.service.GetAllAccountOfUser(userId)
if e, ok := err.(*model.APIError); ok { if e, ok := err.(*model.APIError); ok {
utils.GetLoggerFromCtx(gc).WithError(err).WithField("type", e.Type).Error("error GetAllAccounts: get accounts") utils.GetLoggerFromCtx(gc).WithError(err).WithField("type", e.Type).Error("error GetAllAccounts: get accounts")
@@ -58,26 +39,11 @@ func (c *Context) GetAllAccountOfUser(gc *gin.Context) {
func (c *Context) CreateAccountOfUser(gc *gin.Context) { func (c *Context) CreateAccountOfUser(gc *gin.Context) {
userId := gc.Param("userId") userId := gc.Param("userId")
err := c.Validator.VarCtx(gc, userId, "uuid4")
if err != nil {
utils2.JSONError(gc.Writer, validators.NewDataValidationAPIError(err))
return
}
_, err = c.userService.GetUserById(userId)
if e, ok := err.(*model.APIError); ok {
utils.GetLogger().Info(err)
utils.GetLoggerFromCtx(gc).WithError(err).WithField("type", e.Type).Error("error GetUser: get user error")
utils.JSONErrorWithMessage(gc.Writer, *e, e.Description)
return
} else if err != nil {
utils.GetLoggerFromCtx(gc).WithError(err).Error("error while get user")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
}
var account Account var account Account
var accountEditable AccountEditable var accountEditable AccountEditable
if err := gc.BindJSON(&accountEditable); err != nil { if err := gc.BindJSON(&accountEditable); err != nil {
utils2.JSONError(gc.Writer, validators.NewDataValidationAPIError(err)) utils.JSONError(gc.Writer, validators.NewDataValidationAPIError(err))
return return
} }
account = Account{AccountEditable: accountEditable, UserId: userId} account = Account{AccountEditable: accountEditable, UserId: userId}
@@ -112,127 +78,18 @@ func (c *Context) CreateAccountOfUser(gc *gin.Context) {
func (c *Context) DeleteAccountOfUser(gc *gin.Context) { func (c *Context) DeleteAccountOfUser(gc *gin.Context) {
userId := gc.Param("userId") userId := gc.Param("userId")
err := c.Validator.VarCtx(gc, userId, "uuid4")
if err != nil {
utils2.JSONError(gc.Writer, validators.NewDataValidationAPIError(err))
return
}
accountId := gc.Param("accountId") accountId := gc.Param("accountId")
err = c.Validator.VarCtx(gc, userId, "uuid4")
if err != nil {
utils2.JSONError(gc.Writer, validators.NewDataValidationAPIError(err))
return
}
usrParam, err := c.userService.GetUserById(userId)
if e, ok := err.(*model.APIError); ok {
utils.GetLogger().Info(err)
utils.GetLoggerFromCtx(gc).WithError(err).WithField("type", e.Type).Error("error GetUser: get user error")
utils.JSONErrorWithMessage(gc.Writer, *e, e.Description)
return
} else if err != nil {
utils.GetLoggerFromCtx(gc).WithError(err).Error("error while get user")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
}
googleUserId, exists := gc.Get("googleUserId")
if exists == false {
utils.GetLoggerFromCtx(gc).Error("error while getting google user id")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
}
usr, err := c.userService.GetUserFromGoogleID(googleUserId.(string))
if e, ok := err.(*model.APIError); ok {
utils.GetLogger().Info(err)
utils.GetLoggerFromCtx(gc).WithError(err).WithField("type", e.Type).Error("error GetUserFromGoogleID: get user from google user id")
utils.JSONErrorWithMessage(gc.Writer, *e, e.Description)
return
} else if err != nil {
utils.GetLoggerFromCtx(gc).WithError(err).Error("error while get user from google user id")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
}
if usr == nil || usr.ID != usrParam.ID {
utils.GetLoggerFromCtx(gc).WithError(err).Error("User in path doesn't match authenticated user")
utils.JSONError(gc.Writer, model.ErrBadRequestFormat)
return
}
c.service.DeleteAccountOfUser(userId, accountId) c.service.DeleteAccountOfUser(userId, accountId)
} }
func (c *Context) GetSpecificAccountOfUser(gc *gin.Context) { func (c *Context) GetSpecificAccountOfUser(gc *gin.Context) {
userId := gc.Param("userId") userId := gc.Param("userId")
err := c.Validator.VarCtx(gc, userId, "uuid4")
if err != nil {
utils2.JSONError(gc.Writer, validators.NewDataValidationAPIError(err))
return
}
accountId := gc.Param("accountId") accountId := gc.Param("accountId")
err = c.Validator.VarCtx(gc, userId, "uuid4") account, _ := c.service.GetASpecificAccountForUser(userId, accountId)
if err != nil { utils.JSON(gc.Writer, http.StatusOK, account)
utils2.JSONError(gc.Writer, validators.NewDataValidationAPIError(err))
return
} }
usrParam, err := c.userService.GetUserById(userId) func NewHandler(validator *Validator, database *Database, service *Service, userService *user.Service) *Context {
if e, ok := err.(*model.APIError); ok { return &Context{service: service, db: database, userService: userService, validator: validator}
utils.GetLogger().Info(err)
utils.GetLoggerFromCtx(gc).WithError(err).WithField("type", e.Type).Error("error GetUser: get user error")
utils.JSONErrorWithMessage(gc.Writer, *e, e.Description)
return
} else if err != nil {
utils.GetLoggerFromCtx(gc).WithError(err).Error("error while get user")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
}
googleUserId, exists := gc.Get("googleUserId")
if exists == false {
utils.GetLoggerFromCtx(gc).Error("error while getting google user id")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
}
usr, err := c.userService.GetUserFromGoogleID(googleUserId.(string))
if e, ok := err.(*model.APIError); ok {
utils.GetLogger().Info(err)
utils.GetLoggerFromCtx(gc).WithError(err).WithField("type", e.Type).Error("error GetUserFromGoogleID: get user from google user id")
utils.JSONErrorWithMessage(gc.Writer, *e, e.Description)
return
} else if err != nil {
utils.GetLoggerFromCtx(gc).WithError(err).Error("error while get user from google user id")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
}
if usr == nil || usr.ID != usrParam.ID {
utils.GetLoggerFromCtx(gc).WithError(err).Error("User in path doesn't match authenticated user")
utils.JSONError(gc.Writer, model.ErrBadRequestFormat)
return
}
account, err := c.service.GetASpecificAccountForUser(usr.ID, accountId)
if e, ok := err.(*model.APIError); ok {
utils.GetLogger().Info(err)
utils.GetLoggerFromCtx(gc).WithError(err).WithField("type", e.Type).Error("error GetUserFromGoogleID: get user from google user id")
utils.JSONErrorWithMessage(gc.Writer, *e, e.Description)
return
} else if err != nil {
utils.GetLoggerFromCtx(gc).WithError(err).Error("error while get user from google user id")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
}
utils.JSON(gc.Writer, http.StatusCreated, account)
}
func NewHandler(ctx *handler.Context, db *postgresql.DatabasePostgreSQL) *Context {
database := NewDatabase(db)
service := NewService(database)
userService := user.NewService(user.NewDatabase(db))
return &Context{service: service, db: database, userService: userService, Context: ctx}
} }

34
internal/account/setup.go Normal file
View File

@@ -0,0 +1,34 @@
package account
import (
"github.com/gin-gonic/gin"
"net/http"
"nos-comptes/internal/ginserver"
"nos-comptes/internal/storage/dao/postgresql"
"nos-comptes/internal/user"
"nos-comptes/internal/utils"
)
const ServiceInjectorKey = "ACCOUNT_SERVICE"
func Setup(injector *utils.Injector) {
pg := utils.Get[*postgresql.DatabasePostgreSQL](injector, postgresql.DatabaseKey)
userService := utils.Get[*user.Service](injector, user.ServiceInjectorKey)
database := NewDatabase(pg)
service := NewService(database)
validator := NewValidator(service)
handler := NewHandler(validator, database, service, userService)
securedRoute := utils.Get[*gin.RouterGroup](injector, ginserver.SecuredRouterInjectorKey)
securedUserRoute := securedRoute.Group("/:userId")
//account route
securedUserRoute.Handle(http.MethodGet, "/accounts", handler.GetAllAccountOfUser)
securedUserRoute.Handle(http.MethodPost, "/accounts", handler.CreateAccountOfUser)
securedValidAccount := securedUserRoute.Group("/accounts/:accountId")
securedValidAccount.Use(validator.HasValidAccountId)
securedValidAccount.Use(validator.AccountExists)
securedValidAccount.Handle(http.MethodDelete, "", handler.DeleteAccountOfUser)
securedValidAccount.Handle(http.MethodGet, "", handler.GetSpecificAccountOfUser)
injector.Set(ServiceInjectorKey, service)
}

View File

@@ -0,0 +1,44 @@
package account
import (
"gopkg.in/go-playground/validator.v9"
"nos-comptes/internal/storage/model"
"nos-comptes/internal/storage/validators"
"nos-comptes/internal/utils"
"github.com/gin-gonic/gin"
)
type Validator struct {
accountService *Service
Validator validator.Validate
}
func NewValidator(service *Service) *Validator {
return &Validator{accountService: service}
}
func (v Validator) HasValidAccountId(gc *gin.Context) {
accountId := gc.Param("accountId")
err := v.Validator.VarCtx(gc, accountId, "uuid4")
if err != nil {
utils.JSONError(gc.Writer, validators.NewDataValidationAPIError(err))
return
}
}
func (v Validator) AccountExists(gc *gin.Context) {
userId := gc.Param("userId")
accountId := gc.Param("accountId")
_, err := v.accountService.GetASpecificAccountForUser(userId, accountId)
if e, ok := err.(*model.APIError); ok {
utils.GetLogger().Info(err)
utils.GetLoggerFromCtx(gc).WithError(err).WithField("type", e.Type).Error("error GetUserFromGoogleID: get user from google user id")
utils.JSONErrorWithMessage(gc.Writer, *e, e.Description)
return
} else if err != nil {
utils.GetLoggerFromCtx(gc).WithError(err).Error("error while get user from google user id")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
}
}

View File

@@ -1,11 +1,86 @@
package expense package expense
import "nos-comptes/internal/storage/dao/postgresql" import (
"nos-comptes/internal/storage/dao/postgresql"
"nos-comptes/internal/utils"
"github.com/lib/pq"
)
type Database struct { type Database struct {
*postgresql.DatabasePostgreSQL *postgresql.DatabasePostgreSQL
} }
func (db *Database) CreateExpense(expense *Expense) error {
q := `
INSERT INTO public.expense
(account_id, value, type_expense, expense_date, libelle)
VALUES
($1, $2, $3, $4, $5)
RETURNING id, created_at
`
err := db.Session.
QueryRow(q, expense.AccountId, expense.Value, expense.TypeExpense, expense.ExpenseDate, expense.Libelle).
Scan(&expense.ID, &expense.CreatedAt)
if err != nil {
utils.GetLogger().Info(err)
}
if errPq, ok := err.(*pq.Error); ok {
return postgresql.HandlePgError(errPq)
}
return err
}
func (db Database) GetExpensesOfAnAccountBetween(id, from, to string) ([]*Expense, error) {
q := `
SELECT a.id, a.account_id, a.value, a.type_expense, a.expense_date, a.created_at, a.updated_at, a.libelle
FROM public.expense a
WHERE a.account_id = $1
AND a.expense_date BETWEEN $2 and $3
`
rows, err := db.Session.Query(q, id, from, to)
if err != nil {
return nil, err
}
defer rows.Close()
es := make([]*Expense, 0)
for rows.Next() {
e := Expense{}
err := rows.Scan(&e.ID, &e.AccountId, &e.Value, &e.TypeExpense, &e.ExpenseDate, &e.CreatedAt, &e.UpdatedAt, &e.Libelle)
if err != nil {
return nil, err
}
es = append(es, &e)
}
return es, nil
}
func (db Database) GetAllExpensesOfAnAccount(id string) ([]*Expense, error) {
q := `
SELECT a.id, a.account_id, a.value, a.type_expense, a.expense_date, a.created_at, a.updated_at, a.libelle
FROM public.expense a
WHERE a.account_id = $1
`
rows, err := db.Session.Query(q, id)
if err != nil {
return nil, err
}
defer rows.Close()
es := make([]*Expense, 0)
for rows.Next() {
e := Expense{}
err := rows.Scan(&e.ID, &e.AccountId, &e.Value, &e.TypeExpense, &e.ExpenseDate, &e.CreatedAt, &e.UpdatedAt, &e.Libelle)
if err != nil {
return nil, err
}
es = append(es, &e)
}
return es, nil
}
func NewDatabase(db *postgresql.DatabasePostgreSQL) *Database { func NewDatabase(db *postgresql.DatabasePostgreSQL) *Database {
return &Database{db} return &Database{db}
} }

View File

@@ -1,8 +1,14 @@
package expense package expense
import ( import (
"nos-comptes/handler" "encoding/csv"
"nos-comptes/internal/storage/dao/postgresql" "gopkg.in/go-playground/validator.v9"
"net/http"
"nos-comptes/internal/account"
"nos-comptes/internal/storage/model"
"nos-comptes/internal/storage/validators"
"nos-comptes/internal/utils"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -10,27 +16,113 @@ import (
type Context struct { type Context struct {
service *Service service *Service
db *Database db *Database
*handler.Context accountService *account.Service
validator *validator.Validate
} }
func (c *Context) CreateAnExpense(context *gin.Context) { func (c *Context) ImportExpenseFromCSV(gc *gin.Context) {
}
func (c *Context) CreateAnExpense(gc *gin.Context) {
accountID := gc.Param("accountId")
userId := gc.Param("userId")
csvHeaderFile, err := gc.FormFile("attachment")
if err != nil {
utils.GetLogger().Info(err)
utils.JSONErrorWithMessage(gc.Writer, model.ErrInternalServer, err.Error())
return
}
if err == nil {
csvFile, err := csvHeaderFile.Open()
if err != nil {
utils.GetLogger().Info(err)
utils.JSONErrorWithMessage(gc.Writer, model.ErrInternalServer, err.Error())
return
}
csvr := csv.NewReader(csvFile)
csvr.FieldsPerRecord = -1
csvr.Comma = ';'
filedata, err := csvr.ReadAll()
account, err := c.accountService.GetASpecificAccountForUser(userId, accountID)
if err != nil {
utils.GetLogger().Info(err)
utils.JSONErrorWithMessage(gc.Writer, model.ErrInternalServer, err.Error())
return
}
err = c.service.ProcessCSVFile(filedata, account)
if err != nil {
utils.JSONErrorWithMessage(gc.Writer, model.ErrInternalServer, err.Error())
return
}
return
}
var expense Expense
var expenseEditable ExpenseEditable
if err := gc.BindJSON(&expenseEditable); err != nil {
utils.JSONError(gc.Writer, validators.NewDataValidationAPIError(err))
return
}
expense = Expense{ExpenseEditable: expenseEditable, AccountId: accountID}
err = c.service.CreateExpense(&expense)
if err != nil {
utils.GetLogger().Info(err)
utils.JSONErrorWithMessage(gc.Writer, model.ErrInternalServer, err.Error())
return
}
utils.JSON(gc.Writer, http.StatusCreated, expense)
}
func (c *Context) DeleteExpense(gc *gin.Context) {
} }
func (c *Context) DeleteExpense(context *gin.Context) { func (c *Context) GetAllExpenses(gc *gin.Context) {
accountId := gc.Param("accountId")
from := gc.Query("from")
to := gc.Query("to")
var expenses []*Expense
var err error
if from != "" || to != "" {
if to == "" {
fromParsed, err := time.Parse("2006-01-02", from)
if err == nil {
to = time.Now().Format("2006-01-02")
} else {
to = fromParsed.AddDate(0, 1, 0).Format("2006-01-02")
}
}
if from == "" {
toParsed, err := time.Parse("2006-01-02", to)
if err == nil {
from = "1900-01-01"
} else {
from = toParsed.AddDate(0, -1, 0).Format("2006-01-02")
}
}
expenses, err = c.service.GetExpensesOfAnAccountBetween(accountId, from, to)
} else {
expenses, err = c.service.GetAllExpensesOfAnAccount(accountId)
}
if e, ok := err.(*model.APIError); ok {
utils.GetLoggerFromCtx(gc).WithError(err).WithField("type", e.Type).Error("error GetAllExpenses: get expenses")
utils.JSONErrorWithMessage(gc.Writer, *e, e.Description)
} else if err != nil {
utils.GetLoggerFromCtx(gc).WithError(err).Error("error while get expenses")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
} }
func (c *Context) GetAllExpenses(context *gin.Context) { if len(expenses) == 0 {
utils.JSON(gc.Writer, http.StatusNoContent, nil)
} else {
utils.JSON(gc.Writer, http.StatusOK, expenses)
}
} }
func (c *Context) GetAnExpenses(context *gin.Context) { func (c *Context) GetAnExpenses(context *gin.Context) {
} }
func NewHandler(ctx *handler.Context, db *postgresql.DatabasePostgreSQL) *Context { func NewHandler(validator *validator.Validate, database *Database, service *Service, accountService *account.Service) *Context {
database := NewDatabase(db) return &Context{service: service, db: database, accountService: accountService, validator: validator}
service := NewService(database)
return &Context{service: service, db: database, Context: ctx}
} }

View File

@@ -1,4 +1,18 @@
package expense package expense
type Account struct { import "time"
type Expense struct {
ExpenseEditable
AccountId string `json:"accountId,omitempty"`
}
type ExpenseEditable struct {
ID string `json:"id,omitempty"`
Value float32 `json:"value"`
Libelle string `json:"libelle"`
TypeExpense string `json:"typeExpense"`
ExpenseDate time.Time `json:"expenseDate,omitempty"`
CreatedAt *time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
} }

View File

@@ -1,9 +1,120 @@
package expense package expense
import (
"nos-comptes/internal/account"
"nos-comptes/internal/storage/dao"
"nos-comptes/internal/storage/model"
"nos-comptes/internal/utils"
"strconv"
"strings"
"time"
)
type Service struct { type Service struct {
Db *Database db *Database
}
func (s Service) GetExpensesOfAnAccountBetween(accountId, from, to string) ([]*Expense, error) {
expenses, err := s.db.GetExpensesOfAnAccountBetween(accountId, from, to)
utils.GetLogger().Info(err)
if e, ok := err.(*dao.Error); ok {
switch {
case e.Type == dao.ErrTypeNotFound:
return nil, &model.ErrNotFound
default:
return nil, &model.ErrInternalServer
}
} else if err != nil {
return nil, &model.ErrInternalServer
}
if expenses == nil {
return nil, &model.ErrNotFound
}
return expenses, nil
}
func (s Service) GetAllExpensesOfAnAccount(accountId string) ([]*Expense, error) {
expenses, err := s.db.GetAllExpensesOfAnAccount(accountId)
utils.GetLogger().Info(err)
if e, ok := err.(*dao.Error); ok {
switch {
case e.Type == dao.ErrTypeNotFound:
return nil, &model.ErrNotFound
default:
return nil, &model.ErrInternalServer
}
} else if err != nil {
return nil, &model.ErrInternalServer
}
if expenses == nil {
return nil, &model.ErrNotFound
}
return expenses, nil
}
func (s Service) CreateExpense(expense *Expense) error {
return s.db.CreateExpense(expense)
}
func (s Service) ProcessCSVFile(filedata [][]string, account *account.Account) error {
switch account.Provider {
case "caisse-epargne":
return s.processCaisseEpargne(filedata, account)
case "boursorama":
return s.processBoursorama(filedata, account)
case "bnp":
return s.processBnp(filedata, account)
default:
return nil
}
}
func (s Service) processCaisseEpargne(filedata [][]string, account *account.Account) error {
for _, val := range filedata[4:] {
expenseDate, err := time.Parse("02/01/06", val[0])
if err != nil {
utils.GetLogger().Info(err)
continue
}
amount := val[3]
typeExpense := "D"
if amount == "" {
amount = val[4]
typeExpense = "C"
}
amountParsed, err := strconv.ParseFloat(strings.Trim(strings.ReplaceAll(amount, ",", "."), "+"), 32)
if err != nil {
utils.GetLogger().Info(err)
continue
}
expense := &Expense{
ExpenseEditable: ExpenseEditable{
Value: float32(amountParsed),
Libelle: val[2],
TypeExpense: typeExpense,
ExpenseDate: expenseDate,
},
AccountId: account.ID,
}
s.CreateExpense(expense)
utils.GetLogger().Info(val)
}
return nil
}
func (s Service) processBoursorama(filedata [][]string, account *account.Account) error {
return nil
}
func (s Service) processBnp(filedata [][]string, account *account.Account) error {
return nil
} }
func NewService(database *Database) *Service { func NewService(database *Database) *Service {
return &Service{Db: database} return &Service{db: database}
} }

36
internal/expense/setup.go Normal file
View File

@@ -0,0 +1,36 @@
package expense
import (
"github.com/gin-gonic/gin"
"gopkg.in/go-playground/validator.v9"
"net/http"
"nos-comptes/internal/account"
"nos-comptes/internal/ginserver"
"nos-comptes/internal/storage/dao/postgresql"
"nos-comptes/internal/utils"
validatorInternal "nos-comptes/internal/utils/validator"
)
const ServiceInjectorKey = "EXPENSE_SERVICE"
func Setup(injector *utils.Injector) {
pg := utils.Get[*postgresql.DatabasePostgreSQL](injector, postgresql.DatabaseKey)
validate := utils.Get[*validator.Validate](injector, validatorInternal.ValidatorInjectorKey)
accountService := utils.Get[*account.Service](injector, account.ServiceInjectorKey)
database := NewDatabase(pg)
service := NewService(database)
handler := NewHandler(validate, database, service, accountService)
securedRoute := utils.Get[*gin.RouterGroup](injector, ginserver.SecuredRouterInjectorKey)
securedUserRoute := securedRoute.Group("/:userId")
securedValidAccount := securedUserRoute.Group("/accounts/:accountId")
injector.Set(ServiceInjectorKey, service)
securedValidAccount.Handle(http.MethodPost, "/expenses", handler.CreateAnExpense)
securedValidAccount.Handle(http.MethodGet, "/expenses", handler.GetAllExpenses)
securedExistingExpenses := securedValidAccount.Group("/expenses/:expenseId")
securedExistingExpenses.Handle(http.MethodGet, "", handler.GetAnExpenses)
securedExistingExpenses.Handle(http.MethodDelete, "", handler.DeleteExpense)
}

View File

@@ -2,7 +2,7 @@ package ginserver
import ( import (
"math/rand" "math/rand"
utils2 "nos-comptes/internal/utils" "nos-comptes/internal/utils"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -37,16 +37,16 @@ func randStringBytesMaskImprSrc(n int) string {
func GetLoggerMiddleware() gin.HandlerFunc { func GetLoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
correlationID := c.Request.Header.Get(utils2.HeaderNameCorrelationID) correlationID := c.Request.Header.Get(utils.HeaderNameCorrelationID)
if correlationID == "" { if correlationID == "" {
correlationID = randStringBytesMaskImprSrc(30) correlationID = randStringBytesMaskImprSrc(30)
c.Writer.Header().Set(utils2.HeaderNameCorrelationID, correlationID) c.Writer.Header().Set(utils.HeaderNameCorrelationID, correlationID)
} }
logger := utils2.GetLogger() logger := utils.GetLogger()
logEntry := logger.WithField(utils2.HeaderNameCorrelationID, correlationID) logEntry := logger.WithField(utils.HeaderNameCorrelationID, correlationID)
c.Set(utils2.ContextKeyLogger, logEntry) c.Set(utils.ContextKeyLogger, logEntry)
} }
} }
@@ -54,7 +54,7 @@ func GetHTTPLoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
start := time.Now() start := time.Now()
utils2.GetLoggerFromCtx(c). utils.GetLoggerFromCtx(c).
WithField("method", c.Request.Method). WithField("method", c.Request.Method).
WithField("url", c.Request.RequestURI). WithField("url", c.Request.RequestURI).
WithField("from", c.ClientIP()). WithField("from", c.ClientIP()).
@@ -63,7 +63,7 @@ func GetHTTPLoggerMiddleware() gin.HandlerFunc {
c.Next() c.Next()
d := time.Since(start) d := time.Since(start)
utils2.GetLoggerFromCtx(c). utils.GetLoggerFromCtx(c).
WithField("status", c.Writer.Status()). WithField("status", c.Writer.Status()).
WithField("duration", d.String()). WithField("duration", d.String()).
Info("end handling HTTP request") Info("end handling HTTP request")

View File

@@ -1,4 +1,4 @@
package middleware package ginserver
import ( import (
"fmt" "fmt"
@@ -16,6 +16,7 @@ func ValidateOAuthToken(c *gin.Context) {
authorizationHeaderSplitted := strings.Split(authorizationHeader, " ") authorizationHeaderSplitted := strings.Split(authorizationHeader, " ")
if len(authorizationHeaderSplitted) != 2 { if len(authorizationHeaderSplitted) != 2 {
utils.JSONError(c.Writer, model.ErrBadRequestFormat) utils.JSONError(c.Writer, model.ErrBadRequestFormat)
c.Abort()
return return
} }
@@ -23,6 +24,7 @@ func ValidateOAuthToken(c *gin.Context) {
if oauth2Service == nil { if oauth2Service == nil {
fmt.Println(err) fmt.Println(err)
utils.JSONError(c.Writer, model.ErrInternalServer) utils.JSONError(c.Writer, model.ErrInternalServer)
c.Abort()
return return
} }
tokenInfoCall := oauth2Service.Tokeninfo() tokenInfoCall := oauth2Service.Tokeninfo()
@@ -31,6 +33,7 @@ func ValidateOAuthToken(c *gin.Context) {
if err != nil { if err != nil {
utils.GetLogger().WithError(err).Error(err) utils.GetLogger().WithError(err).Error(err)
utils.JSONError(c.Writer, model.ErrBadRequestFormat) utils.JSONError(c.Writer, model.ErrBadRequestFormat)
c.Abort()
return return
} }
c.Set("googleUserId", token.UserId) c.Set("googleUserId", token.UserId)

View File

@@ -0,0 +1,47 @@
package ginserver
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"net/http"
"nos-comptes/handler"
"nos-comptes/internal/utils"
"time"
)
var (
routerInjectorKey = "ROUTER"
SecuredRouterInjectorKey = "SECURED_ROUTER"
UnsecuredRouterInjectorKey = "UNSECURED_ROUTER"
)
func Setup(injector *utils.Injector, config *handler.Config) {
gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.HandleMethodNotAllowed = true
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:8080/", "http://localhost:8080", "http://localhost:19006"},
AllowMethods: []string{"*"},
AllowHeaders: []string{"*"},
ExposeHeaders: []string{"*"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
router.Use(gin.Recovery())
router.Use(GetLoggerMiddleware())
router.Use(GetHTTPLoggerMiddleware())
public := router.Group("/")
public.Handle(http.MethodGet, "/_health", handler.GetHealth)
injector.Set(UnsecuredRouterInjectorKey, public)
securedUserRoute := public.Group("/users")
securedUserRoute.Use(ValidateOAuthToken)
injector.Set(SecuredRouterInjectorKey, securedUserRoute)
injector.Set(routerInjectorKey, router)
}

View File

@@ -0,0 +1,38 @@
package ginserver
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
"nos-comptes/internal/utils"
"os"
"os/signal"
"syscall"
)
func Start(injector *utils.Injector) {
router := utils.Get[*gin.Engine](injector, routerInjectorKey)
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
// service connections
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
// kill (no param) default send syscanll.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutdown Server ...")
}

View File

@@ -1,11 +0,0 @@
package sharedaccount
import "nos-comptes/internal/storage/dao/postgresql"
type Database struct {
*postgresql.DatabasePostgreSQL
}
func NewDatabase(db *postgresql.DatabasePostgreSQL) *Database {
return &Database{db}
}

View File

@@ -1,37 +0,0 @@
package sharedaccount
import (
"nos-comptes/handler"
"nos-comptes/internal/storage/dao/postgresql"
"github.com/gin-gonic/gin"
)
type Context struct {
service *Service
db *Database
*handler.Context
}
func (c *Context) ShareAnAccount(context *gin.Context) {
}
func (c *Context) DeleteSharedAccount(context *gin.Context) {
}
func (c *Context) GetAllSharedAccountOfUser(context *gin.Context) {
}
func (c *Context) GetSpecificSharedAccountOfUser(context *gin.Context) {
}
func NewHandler(ctx *handler.Context, db *postgresql.DatabasePostgreSQL) *Context {
database := NewDatabase(db)
service := NewService(database)
return &Context{service: service, db: database, Context: ctx}
}

View File

@@ -1,4 +0,0 @@
package sharedaccount
type SharedAccount struct {
}

View File

@@ -1,9 +0,0 @@
package sharedaccount
type Service struct {
Db *Database
}
func NewService(database *Database) *Service {
return &Service{Db: database}
}

BIN
internal/storage/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,13 @@
package postgresql
import (
"nos-comptes/internal/utils"
)
var DatabaseKey = "POSTGRES"
func Setup(injector *utils.Injector, connectionUri string) {
database := NewDatabasePostgreSQL(connectionUri)
injector.Set(DatabaseKey, database)
}

View File

@@ -1,12 +1,8 @@
package user package user
import ( import (
"fmt"
"net/http" "net/http"
"nos-comptes/handler"
"nos-comptes/internal/storage/dao/postgresql"
"nos-comptes/internal/storage/model" "nos-comptes/internal/storage/model"
"nos-comptes/internal/storage/validators"
"nos-comptes/internal/utils" "nos-comptes/internal/utils"
"strings" "strings"
@@ -17,13 +13,7 @@ import (
type Context struct { type Context struct {
service *Service service *Service
db *Database db *Database
*handler.Context validator *Validator
}
func NewHandler(ctx *handler.Context, db *postgresql.DatabasePostgreSQL) *Context {
database := NewDatabase(db)
service := NewService(database)
return &Context{service: service, db: database, Context: ctx}
} }
func (uc *Context) GetAllUsers(c *gin.Context) { func (uc *Context) GetAllUsers(c *gin.Context) {
@@ -46,7 +36,7 @@ func (hc *Context) ConnectUser(c *gin.Context) {
oauth2Service, err := oauth2.New(&http.Client{}) oauth2Service, err := oauth2.New(&http.Client{})
if oauth2Service == nil { if oauth2Service == nil {
fmt.Println(err) utils.GetLoggerFromCtx(c).WithError(err).Error(err)
utils.JSONError(c.Writer, model.ErrInternalServer) utils.JSONError(c.Writer, model.ErrInternalServer)
return return
} }
@@ -54,18 +44,17 @@ func (hc *Context) ConnectUser(c *gin.Context) {
tokenInfoCall.IdToken(authorizationHeaderSplitted[1]) tokenInfoCall.IdToken(authorizationHeaderSplitted[1])
tokenInfo, err := tokenInfoCall.Do() tokenInfo, err := tokenInfoCall.Do()
if err != nil { if err != nil {
utils.GetLogger().WithError(err).Error(err) utils.GetLoggerFromCtx(c).WithError(err).Error(err)
utils.JSONError(c.Writer, model.ErrBadRequestFormat) utils.JSONError(c.Writer, model.ErrBadRequestFormat)
return return
} }
user, err := hc.service.GetUserFromGoogleID(tokenInfo.UserId) user, err := hc.service.GetUserFromGoogleID(tokenInfo.UserId)
if err != nil { if err != nil {
utils.GetLogger().WithError(err).Error(err)
if castedError, ok := err.(*model.APIError); ok { if castedError, ok := err.(*model.APIError); ok {
if castedError.Type == model.ErrNotFound.Type { if castedError.Type == model.ErrNotFound.Type {
user, err := hc.service.CreateUserFromGoogleToken(tokenInfo.UserId, tokenInfo.Email) user, err := hc.service.CreateUserFromGoogleToken(tokenInfo.UserId, tokenInfo.Email)
if err != nil { if err != nil {
fmt.Println(err) utils.GetLoggerFromCtx(c).WithError(err).Error(err)
utils.JSONError(c.Writer, model.ErrInternalServer) utils.JSONError(c.Writer, model.ErrInternalServer)
return return
} }
@@ -75,11 +64,10 @@ func (hc *Context) ConnectUser(c *gin.Context) {
utils.JSONError(c.Writer, *castedError) utils.JSONError(c.Writer, *castedError)
return return
} }
utils.GetLoggerFromCtx(c).WithError(err).Error(err)
utils.JSONError(c.Writer, model.ErrInternalServer) utils.JSONError(c.Writer, model.ErrInternalServer)
return return
} }
fmt.Println("Found the user " + user.Email)
fmt.Println("Return 200")
utils.JSON(c.Writer, 200, user) utils.JSON(c.Writer, 200, user)
} }
@@ -93,7 +81,7 @@ func (hc *Context) CreateUser(c *gin.Context) {
oauth2Service, err := oauth2.New(&http.Client{}) oauth2Service, err := oauth2.New(&http.Client{})
if oauth2Service == nil { if oauth2Service == nil {
fmt.Println(err) utils.GetLogger().WithError(err).Error(err)
utils.JSONError(c.Writer, model.ErrInternalServer) utils.JSONError(c.Writer, model.ErrInternalServer)
return return
} }
@@ -112,7 +100,7 @@ func (hc *Context) CreateUser(c *gin.Context) {
if castedError.Type == model.ErrNotFound.Type { if castedError.Type == model.ErrNotFound.Type {
user, err := hc.service.CreateUserFromGoogleToken(tokenInfo.UserId, tokenInfo.Email) user, err := hc.service.CreateUserFromGoogleToken(tokenInfo.UserId, tokenInfo.Email)
if err != nil { if err != nil {
fmt.Println(err) utils.GetLogger().WithError(err).Error(err)
utils.JSONError(c.Writer, model.ErrInternalServer) utils.JSONError(c.Writer, model.ErrInternalServer)
return return
} }
@@ -122,6 +110,7 @@ func (hc *Context) CreateUser(c *gin.Context) {
utils.JSONError(c.Writer, *castedError) utils.JSONError(c.Writer, *castedError)
return return
} }
utils.GetLogger().Info(err)
utils.JSONError(c.Writer, model.ErrInternalServer) utils.JSONError(c.Writer, model.ErrInternalServer)
return return
} }
@@ -130,27 +119,10 @@ func (hc *Context) CreateUser(c *gin.Context) {
func (hc *Context) GetUser(c *gin.Context) { func (hc *Context) GetUser(c *gin.Context) {
userID := c.Param("userId") userID := c.Param("userId")
user, _ := hc.service.GetUserById(userID)
err := hc.Validator.VarCtx(c, userID, "uuid4")
if err != nil {
utils.JSONError(c.Writer, validators.NewDataValidationAPIError(err))
return
}
user, err := hc.service.GetUserById(userID)
if e, ok := err.(*model.APIError); ok {
utils.GetLoggerFromCtx(c).WithError(err).WithField("type", e.Type).Error("error GetUser: get user error")
utils.JSONErrorWithMessage(c.Writer, *e, e.Description)
} else if err != nil {
utils.GetLoggerFromCtx(c).WithError(err).Error("error while get user")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
if user == nil {
utils.JSONErrorWithMessage(c.Writer, model.ErrNotFound, "User not found")
return
}
utils.JSON(c.Writer, http.StatusOK, user) utils.JSON(c.Writer, http.StatusOK, user)
} }
func NewHandler(validator *Validator, database *Database, service *Service) *Context {
return &Context{service: service, db: database, validator: validator}
}

30
internal/user/setup.go Normal file
View File

@@ -0,0 +1,30 @@
package user
import (
"github.com/gin-gonic/gin"
"net/http"
"nos-comptes/internal/ginserver"
"nos-comptes/internal/storage/dao/postgresql"
"nos-comptes/internal/utils"
)
const ServiceInjectorKey = "USER_SERVICE"
func Setup(injector *utils.Injector) {
pg := utils.Get[*postgresql.DatabasePostgreSQL](injector, postgresql.DatabaseKey)
database := NewDatabase(pg)
service := NewService(database)
validator := NewValidator(service)
handler := NewHandler(validator, database, service)
securedRoute := utils.Get[*gin.RouterGroup](injector, ginserver.SecuredRouterInjectorKey)
//TODO add secure auth
securedRoute.Handle(http.MethodGet, "/:userId", handler.GetUser)
securedUserRoute := securedRoute.Group("/:userId")
securedUserRoute.Use(validator.HasValidUserId)
securedUserRoute.Use(validator.UserdIdMatchOAuthToken)
injector.Set(ServiceInjectorKey, service)
}
func SetupRoute(injector *utils.Injector) {
}

View File

@@ -0,0 +1,82 @@
package user
import (
"gopkg.in/go-playground/validator.v9"
"nos-comptes/internal/storage/model"
"nos-comptes/internal/storage/validators"
"nos-comptes/internal/utils"
"github.com/gin-gonic/gin"
)
type Validator struct {
userService *Service
Validator validator.Validate
}
func NewValidator(service *Service) *Validator {
return &Validator{userService: service}
}
func (v Validator) HasValidUserId(gc *gin.Context) {
userId := gc.Param("userId")
err := v.Validator.VarCtx(gc, userId, "uuid4")
if err != nil {
utils.JSONError(gc.Writer, validators.NewDataValidationAPIError(err))
return
}
}
func (v Validator) UserExists(gc *gin.Context) {
userId := gc.Param("userId")
user, err := v.userService.GetUserById(userId)
if e, ok := err.(*model.APIError); ok {
utils.GetLoggerFromCtx(gc).WithError(err).WithField("type", e.Type).Error("error GetUser: get user error")
utils.JSONErrorWithMessage(gc.Writer, *e, e.Description)
} else if err != nil {
utils.GetLoggerFromCtx(gc).WithError(err).Error("error while get user")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
}
if user == nil {
utils.JSONErrorWithMessage(gc.Writer, model.ErrNotFound, "User not found")
return
}
}
func (v Validator) UserdIdMatchOAuthToken(gc *gin.Context) {
userId := gc.Param("userId")
usrParam, err := v.userService.GetUserById(userId)
if e, ok := err.(*model.APIError); ok {
utils.GetLoggerFromCtx(gc).WithError(err).WithField("type", e.Type).Error("error GetUser: get user error")
utils.JSONErrorWithMessage(gc.Writer, *e, e.Description)
} else if err != nil {
utils.GetLoggerFromCtx(gc).WithError(err).Error("error while get user")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
}
googleUserId, exists := gc.Get("googleUserId")
if exists == false {
utils.GetLoggerFromCtx(gc).Error("error while getting google user id")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
}
usr, err := v.userService.GetUserFromGoogleID(googleUserId.(string))
if e, ok := err.(*model.APIError); ok {
utils.GetLogger().Info(err)
utils.GetLoggerFromCtx(gc).WithError(err).WithField("type", e.Type).Error("error GetUserFromGoogleID: get user from google user id")
utils.JSONErrorWithMessage(gc.Writer, *e, e.Description)
return
} else if err != nil {
utils.GetLoggerFromCtx(gc).WithError(err).Error("error while get user from google user id")
utils.JSONError(gc.Writer, model.ErrInternalServer)
return
}
if usr == nil || usr.ID != usrParam.ID {
utils.GetLoggerFromCtx(gc).WithError(err).Error("User in path doesn't match authenticated user")
utils.JSONError(gc.Writer, model.ErrBadRequestFormat)
return
}
}

View File

@@ -0,0 +1,29 @@
package utils
import "fmt"
type Injector struct {
content map[string]any
}
func (i *Injector) Get(key string) any {
val, ok := i.content[key]
if !ok {
panic(fmt.Sprintf("Can't get key %s from injector", key))
}
return val
}
func Get[T any](i *Injector, key string) T {
return i.Get(key).(T)
}
func (i *Injector) Set(key string, content any) {
if i.content == nil {
i.content = map[string]any{}
}
_, ok := i.content[key]
if ok {
panic(fmt.Sprintf("Key %s already have content", key))
}
i.content[key] = content
}

View File

@@ -0,0 +1,12 @@
package validator
import (
"nos-comptes/internal/utils"
)
const ValidatorInjectorKey = "VALIDATOR"
func Setup(injector *utils.Injector) {
injector.Set(ValidatorInjectorKey, newValidator())
}

View File

@@ -0,0 +1,28 @@
package validator
import (
"gopkg.in/go-playground/validator.v9"
"nos-comptes/internal/storage/validators"
"reflect"
"strings"
)
func newValidator() *validator.Validate {
va := validator.New()
va.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)
if len(name) < 1 {
return ""
}
return name[0]
})
for k, v := range validators.CustomValidators {
if v.Validator != nil {
va.RegisterValidationCtx(k, v.Validator)
}
}
return va
}

View File

@@ -8,4 +8,6 @@
<include file="changeset/create-account.xml" relativeToChangelogFile="true"/> <include file="changeset/create-account.xml" relativeToChangelogFile="true"/>
<include file="changeset/create-shared-account.xml" relativeToChangelogFile="true"/> <include file="changeset/create-shared-account.xml" relativeToChangelogFile="true"/>
<include file="changeset/create-expenses.xml" relativeToChangelogFile="true"/> <include file="changeset/create-expenses.xml" relativeToChangelogFile="true"/>
<include file="changeset/create-jointaccount.xml" relativeToChangelogFile="true"/>
<include file="changeset/create-jointexpenses.xml" relativeToChangelogFile="true"/>
</databaseChangeLog> </databaseChangeLog>

View File

@@ -11,6 +11,9 @@
<column name="account_id" type="uuid"> <column name="account_id" type="uuid">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
<column name="libelle" type="text" >
<constraints nullable="true"/>
</column>
<column name="value" type="number"> <column name="value" type="number">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet id="add-jointaccount-table" author="kratisto">
<createTable tableName="jointaccount">
<column name="id" type="uuid" defaultValueComputed="gen_random_uuid()">
<constraints nullable="false" unique="true"/>
</column>
<column name="name" type="text" >
<constraints nullable="true"/>
</column>
<column name="provider" type="text">
<constraints nullable="true"/>
</column>
<column name="user_id" type="uuid" >
<constraints nullable="false"/>
</column>
<column name="created_at" type="timestamp" defaultValueComputed="now()">
<constraints nullable="true"/>
</column>
<column name="updated_at" type="timestamp">
<constraints nullable="true"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet id="add-jointexpenses-table" author="kratisto">
<createTable tableName="jointexpense">
<column name="id" type="uuid" defaultValueComputed="gen_random_uuid()">
<constraints nullable="false" unique="true"/>
</column>
<column name="jointaccount_id" type="uuid">
<constraints nullable="false"/>
</column>
<column name="libelle" type="text" >
<constraints nullable="true"/>
</column>
<column name="value" type="number">
<constraints nullable="false"/>
</column>
<column name="type_jointexpense" type="char(1)">
<constraints nullable="false"/>
</column>
<column name="jointexpense_date" type="timestamp" defaultValueComputed="now()">
<constraints nullable="true"/>
</column>
<column name="created_at" type="timestamp" defaultValueComputed="now()">
<constraints nullable="true"/>
</column>
<column name="updated_at" type="timestamp">
<constraints nullable="true"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>