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
}

103
go.mod
View File

@@ -1,54 +1,55 @@
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
github.com/gin-gonic/gin v1.7.4 github.com/gin-gonic/gin v1.7.4
github.com/lib/pq v1.10.3 github.com/lib/pq v1.10.3
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.2.1 github.com/spf13/cobra v1.2.1
github.com/spf13/viper v1.9.0 github.com/spf13/viper v1.9.0
google.golang.org/api v0.59.0 google.golang.org/api v0.59.0
gopkg.in/go-playground/validator.v9 v9.31.0 gopkg.in/go-playground/validator.v9 v9.31.0
) )
require ( require (
cloud.google.com/go v0.97.0 // indirect cloud.google.com/go v0.97.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
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/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/gocarina/gocsv v0.0.0-20211020200912-82fc2684cc48 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/googleapis/gax-go/v2 v2.1.1 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/json-iterator/go v1.1.11 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/leodido/go-urn v1.2.0 // indirect github.com/json-iterator/go v1.1.11 // indirect
github.com/magiconair/properties v1.8.5 // indirect github.com/leodido/go-urn v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect github.com/magiconair/properties v1.8.5 // indirect
github.com/mitchellh/mapstructure v1.4.2 // indirect github.com/mattn/go-isatty v0.0.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/spf13/afero v1.6.0 // indirect github.com/pelletier/go-toml v1.9.4 // indirect
github.com/spf13/cast v1.4.1 // indirect github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect github.com/subosito/gotenv v1.2.0 // indirect
go.opencensus.io v0.23.0 // indirect github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect go.opencensus.io v0.23.0 // indirect
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 // indirect golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 // indirect golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 // indirect
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 // indirect
golang.org/x/text v0.3.6 // indirect golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect
google.golang.org/appengine v1.6.7 // indirect golang.org/x/text v0.3.6 // indirect
google.golang.org/genproto v0.0.0-20211008145708-270636b82663 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/grpc v1.40.0 // indirect google.golang.org/genproto v0.0.0-20211008145708-270636b82663 // indirect
google.golang.org/protobuf v1.27.1 // indirect google.golang.org/grpc v1.40.0 // indirect
gopkg.in/ini.v1 v1.63.2 // indirect google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/ini.v1 v1.63.2 // indirect
) gopkg.in/yaml.v2 v2.4.0 // 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)
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
}
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 { func NewHandler(validator *Validator, database *Database, service *Service, userService *user.Service) *Context {
database := NewDatabase(db) return &Context{service: service, db: database, userService: userService, validator: validator}
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,36 +1,128 @@
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"
) )
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")
func (c *Context) GetAllExpenses(context *gin.Context) { 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
}
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"
@@ -15,15 +11,9 @@ 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>