commit 5e0d0ec69f6df57f925e2d4a232903124056365f Author: Jeffrey Duroyon Date: Fri Jul 19 17:04:42 2024 +0200 wip diff --git a/mangezmieux-backend/.github/workflows/build.yaml b/mangezmieux-backend/.github/workflows/build.yaml new file mode 100644 index 0000000..a1f7538 --- /dev/null +++ b/mangezmieux-backend/.github/workflows/build.yaml @@ -0,0 +1,32 @@ +name: Build + +on: + workflow_dispatch: + push: + branches: + - '**' +jobs: + build: + runs-on: ubuntu-latest + env: + GOPRIVATE: github.com/kratisto + GONOSUMDB: "*github.com/kratisto/mangezmieux-backend" + steps: + - uses: actions/checkout@v3 + + - name: Configure git for private modules + env: + TOKEN: ${{ secrets.GHA_TOKEN_PAT }} + run: git config --global url."https://YOUR_GITHUB_USERNAME:${TOKEN}@github.com".insteadOf "https://github.com" + + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... diff --git a/mangezmieux-backend/.github/workflows/release.yaml b/mangezmieux-backend/.github/workflows/release.yaml new file mode 100644 index 0000000..f9e8552 --- /dev/null +++ b/mangezmieux-backend/.github/workflows/release.yaml @@ -0,0 +1,21 @@ +name: Release + +on: + workflow_dispatch: + push: + branches: + - 'main' +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Semantic Release + id: semantic # Need an `id` for output variables + uses: cycjimmy/semantic-release-action@v4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: rickstaa/action-create-tag@v1 + id: "tag_create" + with: + tag: "pkg/client/v${{ steps.semantic.outputs.new_release_version }}" diff --git a/mangezmieux-backend/.gitignore b/mangezmieux-backend/.gitignore new file mode 100644 index 0000000..8b05a92 --- /dev/null +++ b/mangezmieux-backend/.gitignore @@ -0,0 +1,5 @@ +mangezmieux-backend +vendor/ +.idea/ + +*.iml diff --git a/mangezmieux-backend/.releaserc b/mangezmieux-backend/.releaserc new file mode 100644 index 0000000..6c17de5 --- /dev/null +++ b/mangezmieux-backend/.releaserc @@ -0,0 +1,38 @@ +{ + "branches": ["main"], + "tagFormat":"v${version}", + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "angular", + "releaseRules": [ + {"type": "chore", "release": "patch"} + ] + } + ], + "@semantic-release/release-notes-generator", + [ + "@semantic-release/changelog", + { + "changelogFile": "CHANGELOG.md" + } + ], + [ + "@semantic-release/git", + { + "assets": ["CHANGELOG.md"], + "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" + } + ], + "@semantic-release/github", + [ + "@semantic-release/git", + { + "assets": ["CHANGELOG.md"], + "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" + } + ] + ] +} + diff --git a/mangezmieux-backend/LICENSE b/mangezmieux-backend/LICENSE new file mode 100644 index 0000000..5e938a7 --- /dev/null +++ b/mangezmieux-backend/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Jeffrey Duroyon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mangezmieux-backend/Makefile b/mangezmieux-backend/Makefile new file mode 100644 index 0000000..c2d4489 --- /dev/null +++ b/mangezmieux-backend/Makefile @@ -0,0 +1,82 @@ +# Set an output prefix, which is the local directory if not specified +PREFIX?=$(shell pwd) +.DEFAULT_GOAL := build + +DOCKER_IMAGE_NAME := mangezmieux-backend +# Setup name variables for the package/tool +NAME := mangezmieux-backend +BIN_NAME := mangezmieux-backend +PKG := github.com/kratisto/mangezmieux-backend + +# GO env vars +ifeq ($(GOPATH),) + GOPATH:=~/go +endif +GO=$(firstword $(subst :, ,$(GOPATH))) + +.PHONY: ensure-vendor +ensure-vendor: ## Get all vendor dependencies + @echo "+ $@" + dep ensure + +.PHONY: update-vendor +update-vendor: ## Get all vendor dependencies + @echo "+ $@" + dep ensure -update + +.PHONY: clean +clean: local-clean ## Clean your generated files + @echo "+ $@" + docker image rm mangezmieux:snapshot || true + +.PHONY: local-build +local-build: local-clean local-format ## Build locally the binary + @echo "+ $@" + go build -o $(GO)/bin/$(BIN_NAME) . + +.PHONY: local-clean +local-clean: ## Cleanup locally any build binaries or packages + @echo "+ $@" + @$(RM) $(BIN_NAME) + +.PHONY: local-format +local-format: ## format locally all files + @echo "+ $@" + gofmt -s -l -w . + +.PHONY: build +build: sources-image ## Build the docker image with application binary + @echo "+ $@" + docker build --no-cache \ + -f containers/Dockerfile \ + --build-arg SOURCES_IMAGE=mangezmieux-sources:snapshot \ + -t mangezmieux:snapshot . + +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 mangezmieux-sources:snapshot -f containers/Dockerfile.sources . + +.PHONY: liquibase +liquibase: ## Run the dependencies of the server (launch the containers/docker-compose.local.yml) + @echo "+ $@" + @docker run --network host --rm -v ./liquibase/changelogs:/liquibase/changelog liquibase/liquibase --defaults-file=/liquibase/changelog/liquibase.properties --changelog-file=changelog-master.xml --classpath=changelog update + +.PHONY: local-run-golang +local-run-golang: ## Build the server and run it + @echo "+ $@" + BUILD_GOLANG_CMD=local-build \ + LAUNCH_GOLANG_CMD="$(GO)/bin/$(BIN_NAME)" \ + $(MAKE) local-launch-golang + +.PHONY: local-launch-golang +local-launch-golang: ## Build the server and run it + @echo "+ $@" + PID=`ps -ax | egrep "\b$(BIN_NAME)"| cut -d " " -f 1`; \ + kill $$PID || true + $(MAKE) $(BUILD_GOLANG_CMD) + DB_PORT=`` $(LAUNCH_GOLANG_CMD) serve --loglevel debug --logformat text --postgreshost localhost:$$DB_PORT + +.PHONY: local-run +local-run: local-run-golang ## Run the server with its dependencies diff --git a/mangezmieux-backend/README.md b/mangezmieux-backend/README.md new file mode 100644 index 0000000..11095e1 --- /dev/null +++ b/mangezmieux-backend/README.md @@ -0,0 +1 @@ +# mangezmieux-backend diff --git a/mangezmieux-backend/cmd/root.go b/mangezmieux-backend/cmd/root.go new file mode 100644 index 0000000..59851b0 --- /dev/null +++ b/mangezmieux-backend/cmd/root.go @@ -0,0 +1,106 @@ +package cmd + +import ( + "fmt" + "mangezmieux-backend/configuration" + "mangezmieux-backend/internal/logger" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const ( + // Log. + parameterLogLevel = "loglevel" + parameterLogFormat = "logformat" + + defaultLogLevel = "debug" + defaultLogFormat = "text" + + // Mock. + parameterMock = "mock" + + defaultMock = true + + // Router. + parameterPort = "port" + defaultPort = "8080" + + // DATABASE. + parameterPostgresDBName = "postgresdbname" + defaultPostgresDBName = "mangezmieux" + parameterPostgresDBSchema = "postgresdbschema" + defaultPostgresDBSchema = "mangezmieux" + parameterPostgresHost = "postgreshost" + defaultPostgresHost = "localhost" + parameterPostgresUser = "postgresuser" + defaultPostgresUser = "postgres" + parameterPostgresPwd = "postgrespwd" + defaultPostgresPwd = "mysecretpassword" +) + +var ( + config = &configuration.Config{} + + cfgFile string + // GITHASH : Stores the git revision to be displayed. + GITHASH string + // VERSION : Stores the binary version to be displayed. + VERSION string + + // rootCmd represents the base command when called without any subcommands. + rootCmd = &cobra.Command{ + Use: "mangezmieux", + Short: "mangezmieux", + Version: fmt.Sprintf("%s (%s)", VERSION, GITHASH), + } +) + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + logger.GetLogger().Error(err) + os.Exit(1) + } +} + +func init() { + rootCmd.AddCommand(serveCmd) + + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.mangezmieux.yaml)") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } + + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + logger.GetLogger().Info("Using config file:", viper.ConfigFileUsed()) + } + + config.Mock = viper.GetBool(parameterMock) + + config.Port = viper.GetString(parameterPort) + + config.LogLevel = viper.GetString(parameterLogLevel) + config.LogFormat = viper.GetString(parameterLogFormat) + + config.PostgresDBName = viper.GetString(parameterPostgresDBName) + config.PostgresDBSchema = viper.GetString(parameterPostgresDBSchema) + config.PostgresHost = viper.GetString(parameterPostgresHost) + config.PostgresUser = viper.GetString(parameterPostgresUser) + config.PostgresPwd = viper.GetString(parameterPostgresPwd) + + config.Version = VERSION +} diff --git a/mangezmieux-backend/cmd/serve.go b/mangezmieux-backend/cmd/serve.go new file mode 100644 index 0000000..22021cd --- /dev/null +++ b/mangezmieux-backend/cmd/serve.go @@ -0,0 +1,91 @@ +package cmd + +import ( + "fmt" + "mangezmieux-backend/internal/acl" + aclKey "mangezmieux-backend/internal/acl/key" + "mangezmieux-backend/internal/acl/service" + + "mangezmieux-backend/internal/ginserver" + "mangezmieux-backend/internal/health" + "mangezmieux-backend/internal/injector" + "mangezmieux-backend/internal/jwt" + "mangezmieux-backend/internal/logger" + "mangezmieux-backend/internal/postgres" + "mangezmieux-backend/internal/users" + service2 "mangezmieux-backend/internal/users/service" + coreValidator "mangezmieux-backend/internal/validator" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// serveCmd represents the serve command. +var serveCmd = &cobra.Command{ + Use: "serve", + Short: "Serve endpoints", + PreRun: initServeBindingsFlags, + Run: func(cmd *cobra.Command, args []string) { + serve() + }, +} + +func serve() { + initConfig() + logger.InitLogger(config.LogLevel, config.LogFormat) + logrus. + WithField(parameterLogLevel, config.LogLevel). + WithField(parameterLogFormat, config.LogFormat). + WithField(parameterPort, config.Port). + WithField(parameterPostgresHost, config.PostgresHost). + Warn("Configuration") + inj := &injector.Injector{} + authMiddleware := users.AuthMiddleware // <= its not configured yet + + jwt.Setup(inj) + jwtService := injector.Get[*jwt.Service](inj, jwt.JWTKey) + inj.Set("AuthenticationMiddleware", authMiddleware.GinMiddleware(jwtService)) + + ginserver.Setup(inj) + coreValidator.Setup(inj) + + psqlInfo := fmt.Sprintf("host=localhost port=5432 user=%s "+ + "password=mysecretpassword dbname=%s sslmode=disable", + config.PostgresUser, config.PostgresDBName) + postgres.Setup(inj, psqlInfo) + + health.Setup(inj) + acl.SetupDao(inj) + users.Setup(inj) + + acl.Setup(inj) + + authMiddleware.Service = injector.Get[*service2.Service](inj, users.ServiceKey) + authMiddleware.RoleService = injector.Get[service.Service](inj, aclKey.ServiceKey) + ginserver.Start(inj, config.Port) +} + +func init() { + serveCmd.Flags().String(parameterLogLevel, defaultLogLevel, "Use this flag to set the logging level") + serveCmd.Flags().String(parameterLogFormat, defaultLogFormat, "Use this flag to set the logging format") + serveCmd.Flags().Bool(parameterMock, defaultMock, "Use this flag to mock external services") + serveCmd.Flags().String(parameterPort, defaultPort, "Use this flag to set the listening port of the api") + serveCmd.Flags().String(parameterPostgresDBName, defaultPostgresDBName, "Use this flag to set database name") + serveCmd.Flags().String(parameterPostgresDBSchema, defaultPostgresDBSchema, "Use this flag to set database schema name") + serveCmd.Flags().String(parameterPostgresHost, defaultPostgresHost, "Use this flag to set database host") + serveCmd.Flags().String(parameterPostgresUser, defaultPostgresUser, "Use this flag to set database user name") + serveCmd.Flags().String(parameterPostgresPwd, defaultPostgresPwd, "Use this flag to set database user password") +} + +func initServeBindingsFlags(cmd *cobra.Command, args []string) { + _ = viper.BindPFlag(parameterLogLevel, cmd.Flags().Lookup(parameterLogLevel)) + _ = viper.BindPFlag(parameterLogFormat, cmd.Flags().Lookup(parameterLogFormat)) + _ = viper.BindPFlag(parameterMock, cmd.Flags().Lookup(parameterMock)) + _ = viper.BindPFlag(parameterPort, cmd.Flags().Lookup(parameterPort)) + _ = viper.BindPFlag(parameterPostgresDBName, cmd.Flags().Lookup(parameterPostgresDBName)) + _ = viper.BindPFlag(parameterPostgresDBSchema, cmd.Flags().Lookup(parameterPostgresDBSchema)) + _ = viper.BindPFlag(parameterPostgresHost, cmd.Flags().Lookup(parameterPostgresHost)) + _ = viper.BindPFlag(parameterPostgresUser, cmd.Flags().Lookup(parameterPostgresUser)) + _ = viper.BindPFlag(parameterPostgresPwd, cmd.Flags().Lookup(parameterPostgresPwd)) +} diff --git a/mangezmieux-backend/configuration/config.go b/mangezmieux-backend/configuration/config.go new file mode 100644 index 0000000..0f58417 --- /dev/null +++ b/mangezmieux-backend/configuration/config.go @@ -0,0 +1,19 @@ +package configuration + +// Config holds all the configuration of the application. +type Config struct { + Mock bool + + Port string + + LogLevel string + LogFormat string + + PostgresDBName string + PostgresDBSchema string + PostgresHost string + PostgresUser string + PostgresPwd string + + Version string +} diff --git a/mangezmieux-backend/go.mod b/mangezmieux-backend/go.mod new file mode 100644 index 0000000..67cea79 --- /dev/null +++ b/mangezmieux-backend/go.mod @@ -0,0 +1,67 @@ +module mangezmieux-backend + +go 1.22 + +require ( + github.com/cucumber/godog v0.14.1 + github.com/gin-contrib/cors v1.7.2 + github.com/gin-gonic/gin v1.10.0 + github.com/go-playground/validator/v10 v10.22.0 + github.com/gofrs/uuid v4.3.1+incompatible + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/uuid v1.6.0 + github.com/lib/pq v1.10.9 + github.com/ohler55/ojg v1.22.0 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 + golang.org/x/crypto v0.24.0 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect + github.com/cucumber/messages/go/v21 v21.0.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-memdb v1.3.4 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/mangezmieux-backend/go.sum b/mangezmieux-backend/go.sum new file mode 100644 index 0000000..ed57934 --- /dev/null +++ b/mangezmieux-backend/go.sum @@ -0,0 +1,176 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= +github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= +github.com/cucumber/godog v0.14.1 h1:HGZhcOyyfaKclHjJ+r/q93iaTJZLKYW6Tv3HkmUE6+M= +github.com/cucumber/godog v0.14.1/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces= +github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= +github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= +github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= +github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= +github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c= +github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/ohler55/ojg v1.22.0 h1:McZObj3cD/Zz/ojzk5Pi5VvgQcagxmT1bVKNzhE5ihI= +github.com/ohler55/ojg v1.22.0/go.mod h1:gQhDVpQLqrmnd2eqGAvJtn+NfKoYJbe/A4Sj3/Vro4o= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/mangezmieux-backend/internal/acl/handler.go b/mangezmieux-backend/internal/acl/handler.go new file mode 100644 index 0000000..119d187 --- /dev/null +++ b/mangezmieux-backend/internal/acl/handler.go @@ -0,0 +1,71 @@ +package acl + +import ( + "errors" + "github.com/go-playground/validator/v10" + "mangezmieux-backend/internal/acl/service" + "mangezmieux-backend/internal/logger" + "mangezmieux-backend/internal/middleware" + "mangezmieux-backend/internal/responses" + "mangezmieux-backend/internal/users/model" + + "github.com/gin-gonic/gin" + "net/http" +) + +type Handler struct { + service service.Service + Validator *validator.Validate +} + +const ( + rolePathParam = "roleName" + roleIdPathParam = "roleId" +) + +func NewHandler(service service.Service, validator *validator.Validate) *Handler { + return &Handler{ + service: service, + Validator: validator, + } +} + +func (h Handler) GetAllRole(context *gin.Context) { + roles, err := h.service.GetAllRole() + if err != nil { + logger.GetLogger().Error(err) + var apiError *responses.APIError + if errors.As(err, &apiError) { + responses.JSONError(context.Writer, *apiError) + return + } + responses.JSONErrorWithMessage(context.Writer, responses.ErrInternalServer, err.Error()) + return + } + responses.JSON(context.Writer, http.StatusOK, roles) +} + +func (h Handler) GetMyRoles(context *gin.Context) { + user, exists := context.Get(middleware.CtxUser) + if !exists { + responses.JSONErrorWithMessage(context.Writer, responses.ErrInternalServer, "User not found in context") + return + } + + userModel := user.(*model.User) + + userRight, err := h.service.GetRoleForCurrentUser(userModel) + if err != nil { + logger.GetLogger().Error(err) + + var apiError *responses.APIError + if errors.As(err, &apiError) { + responses.JSONError(context.Writer, *apiError) + return + } + responses.JSONErrorWithMessage(context.Writer, responses.ErrInternalServer, err.Error()) + return + } + responses.JSON(context.Writer, http.StatusOK, userRight) + +} diff --git a/mangezmieux-backend/internal/acl/key/key.go b/mangezmieux-backend/internal/acl/key/key.go new file mode 100644 index 0000000..32ebfd2 --- /dev/null +++ b/mangezmieux-backend/internal/acl/key/key.go @@ -0,0 +1,6 @@ +package key + +const ( + ServiceKey = "RoleService" + DaoKey = "DaoService" +) diff --git a/mangezmieux-backend/internal/acl/model/model.go b/mangezmieux-backend/internal/acl/model/model.go new file mode 100644 index 0000000..d0eb304 --- /dev/null +++ b/mangezmieux-backend/internal/acl/model/model.go @@ -0,0 +1,48 @@ +package model + +import ( + "github.com/google/uuid" + "mangezmieux-backend/internal/model" +) + +type UserRight struct { + UserRole []*UserRole `json:"userRole"` +} + +type Resource struct { + Id uuid.UUID `json:"id"` + Name string `json:"name"` + model.Metadata +} + +type RoleVerbResource struct { + Id uuid.UUID `json:"id"` + Verb string `json:"verb"` + RoleId uuid.UUID `json:"role"` + ResourceId uuid.UUID `json:"resource"` + model.Metadata +} + +type RoleEditable struct { + Id uuid.UUID `json:"id"` + Name string `json:"name"` + model.Metadata +} + +type Role struct { + RoleEditable + ResourceVerb map[string][]*Verb +} + +type Verb struct { + Id uuid.UUID `json:"id"` + Verb string `json:"verb"` + model.Metadata +} + +type UserRole struct { + Id uuid.UUID `json:"id"` + RoleId uuid.UUID `json:"role"` + UserId uuid.UUID `json:"user"` + model.Metadata +} diff --git a/mangezmieux-backend/internal/acl/service/service.go b/mangezmieux-backend/internal/acl/service/service.go new file mode 100644 index 0000000..4c84473 --- /dev/null +++ b/mangezmieux-backend/internal/acl/service/service.go @@ -0,0 +1,60 @@ +package service + +import ( + "github.com/google/uuid" + "mangezmieux-backend/internal/acl/model" + "mangezmieux-backend/internal/acl/sql" + model2 "mangezmieux-backend/internal/users/model" +) + +type service struct { + dao sql.Dao +} + +func (s service) GetRoleForCurrentUser(user *model2.User) (*model.UserRight, error) { + userId, err := uuid.Parse(user.ID.String()) + if err != nil { + return nil, err + } + + userRole, err := s.GetUserRoleByUser(userId) + if err != nil { + return nil, err + } + + userRight := &model.UserRight{ + UserRole: userRole, + } + return userRight, nil +} + +func (s service) GetAllRole() ([]*model.Role, error) { + roles, err := s.dao.GetAllRole() + return roles, err +} + +func (s service) GetUserRoleByUser(id uuid.UUID) ([]*model.UserRole, error) { + userRoles, err := s.dao.GetUserRoleByUser(id) + return userRoles, err +} + +func (s service) GetRole(id uuid.UUID) (*model.Role, error) { + role, err := s.dao.GetRole(id) + if err != nil { + return nil, err + } + + return role, nil +} + +type Service interface { + GetAllRole() ([]*model.Role, error) + GetRoleForCurrentUser(user *model2.User) (*model.UserRight, error) + GetUserRoleByUser(id uuid.UUID) ([]*model.UserRole, error) +} + +func NewService(dao sql.Dao) Service { + return &service{ + dao: dao, + } +} diff --git a/mangezmieux-backend/internal/acl/setup.go b/mangezmieux-backend/internal/acl/setup.go new file mode 100644 index 0000000..33b3fdf --- /dev/null +++ b/mangezmieux-backend/internal/acl/setup.go @@ -0,0 +1,39 @@ +package acl + +import ( + "database/sql" + "mangezmieux-backend/internal/acl/key" + "mangezmieux-backend/internal/acl/service" + aclSql "mangezmieux-backend/internal/acl/sql" + "mangezmieux-backend/internal/ginserver" + "mangezmieux-backend/internal/injector" + "mangezmieux-backend/internal/postgres" + "mangezmieux-backend/internal/validator" + + "net/http" + + "github.com/gin-gonic/gin" + validatorv10 "github.com/go-playground/validator/v10" +) + +func SetupDao(inj *injector.Injector) { + client := injector.Get[*sql.DB](inj, postgres.DatabaseKey) + dao := aclSql.NewDao(client) + inj.Set(key.DaoKey, dao) + +} +func Setup(inj *injector.Injector) { + securedRoute := injector.Get[*gin.RouterGroup](inj, ginserver.SecuredRouterInjectorKey) + validatorCli := injector.Get[*validatorv10.Validate](inj, validator.ValidatorInjectorKey) + dao := injector.Get[aclSql.Dao](inj, key.DaoKey) + aclService := service.NewService(dao) + handler := NewHandler(aclService, validatorCli) + + inj.Set(key.ServiceKey, aclService) + + aclRoute := securedRoute.Group("/roles") + aclRoute.Handle(http.MethodGet, "/", handler.GetAllRole) + + securedRoute.Handle(http.MethodGet, "/users/me/roles", handler.GetMyRoles) + +} diff --git a/mangezmieux-backend/internal/acl/sql/dao.go b/mangezmieux-backend/internal/acl/sql/dao.go new file mode 100644 index 0000000..c8c0a07 --- /dev/null +++ b/mangezmieux-backend/internal/acl/sql/dao.go @@ -0,0 +1,39 @@ +package sql + +import ( + "github.com/google/uuid" + "mangezmieux-backend/internal/acl/model" + model2 "mangezmieux-backend/internal/model" +) + +type Dao interface { + //Role + GetRole(id uuid.UUID) (*model.Role, error) + GetRoleByName(name string) (*model.Role, error) + AddRole(roleName string, metadata model2.Metadata) (*model.Role, error) + DeleteRole(id uuid.UUID) error + GetAllRole() ([]*model.Role, error) + + //Resource + AddResource(resourceName string, metadata model2.Metadata) (*model.Resource, error) + DeleteResource(id uuid.UUID) error + GetResource(id uuid.UUID) (*model.Resource, error) + GetResourceByName(name string) (*model.Resource, error) + GetAllResource() ([]*model.Resource, error) + + //RoleVerbResource + GetRoleVerbResource(id uuid.UUID) (*model.RoleVerbResource, error) + GetRoleVerbResourceByRoleResourceAndVerb(roleId, resourceId uuid.UUID, verb string) (*model.RoleVerbResource, error) + GetRoleVerbResourceByRoleResource(roleId, resourceId uuid.UUID) ([]*model.RoleVerbResource, error) + GetRoleVerbResourceByRole(roleId uuid.UUID) ([]*model.RoleVerbResource, error) + AddRoleVerbResource(roleId, resourceId uuid.UUID, verb string, metadata model2.Metadata) (*model.RoleVerbResource, error) + DeleteRoleVerbResource(id uuid.UUID) error + + //UserRole + GetUserRole(id uuid.UUID) (*model.UserRole, error) + GetUserRoleByUserAndRole(userId, roleId uuid.UUID) (*model.UserRole, error) + GetUserRoleByUser(userId uuid.UUID) ([]*model.UserRole, error) + GetUserRoleByRole(role uuid.UUID) ([]*model.UserRole, error) + AddUserRole(userId, roleId uuid.UUID, metadata model2.Metadata) (*model.UserRole, error) + DeleteUserRole(id uuid.UUID) error +} diff --git a/mangezmieux-backend/internal/acl/sql/sql.go b/mangezmieux-backend/internal/acl/sql/sql.go new file mode 100644 index 0000000..9569625 --- /dev/null +++ b/mangezmieux-backend/internal/acl/sql/sql.go @@ -0,0 +1,13 @@ +package sql + +import ( + "database/sql" +) + +type dao struct { + client *sql.DB +} + +func NewDao(client *sql.DB) Dao { + return &dao{client: client} +} diff --git a/mangezmieux-backend/internal/acl/sql/sql_resource.go b/mangezmieux-backend/internal/acl/sql/sql_resource.go new file mode 100644 index 0000000..9833b36 --- /dev/null +++ b/mangezmieux-backend/internal/acl/sql/sql_resource.go @@ -0,0 +1,121 @@ +package sql + +import ( + "database/sql" + "errors" + "github.com/google/uuid" + "github.com/lib/pq" + "mangezmieux-backend/internal/acl/model" + model2 "mangezmieux-backend/internal/model" + "mangezmieux-backend/internal/postgres" +) + +func (sqlDAO dao) AddResource(resourceName string, metadata model2.Metadata) (*model.Resource, error) { + var Id uuid.UUID + q := ` + INSERT INTO mangezmieux.resource + (name, creation_date, creation_user) + VALUES + ($1, $2, $3) + RETURNING + Id` + + err := sqlDAO.client.QueryRow(q, resourceName, metadata.CreationDate, metadata.CreationUser).Scan(&Id) + + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + + resource, err := sqlDAO.GetResource(Id) + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + + return resource, nil +} + +func (sqlDAO dao) DeleteResource(Id uuid.UUID) error { + q := ` + DELETE FROM mangezmieux.resource + WHERE Id = $1 + ` + + _, err := sqlDAO.client.Exec(q, Id.String()) + var errPq *pq.Error + if errors.As(err, &errPq) { + return postgres.HandlePgError(errPq) + } + return err +} + +func (sqlDAO dao) GetResource(Id uuid.UUID) (*model.Resource, error) { + q := ` + SELECT Id, name, creation_date, last_update_date + FROM mangezmieux.resource r + WHERE r.Id = $1 + ` + row := sqlDAO.client.QueryRow(q, Id.String()) + + resource := &model.Resource{} + err := row.Scan(&resource.Id, &resource.Name, &resource.CreationDate, &resource.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + return resource, nil +} + +func (sqlDAO dao) GetResourceByName(name string) (*model.Resource, error) { + q := ` + SELECT Id, name, creation_date, last_update_date + FROM mangezmieux.resource r + WHERE r.name = $1 + ` + row := sqlDAO.client.QueryRow(q, name) + + resource := &model.Resource{} + err := row.Scan(&resource.Id, &resource.Name, &resource.CreationDate, &resource.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + return resource, nil +} + +func (sqlDAO dao) GetAllResource() ([]*model.Resource, error) { + q := ` + SELECT Id, name, creation_date, last_update_date + FROM mangezmieux.resource r + ` + rows, err := sqlDAO.client.Query(q) + if err != nil { + return nil, err + } + + if rows.Err() != nil { + return nil, rows.Err() + } + + defer rows.Close() + resources := make([]*model.Resource, 0) + for rows.Next() { + resource := &model.Resource{} + err := rows.Scan(&resource.Id, &resource.Name, &resource.CreationDate, &resource.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + resources = append(resources, resource) + } + return resources, nil +} diff --git a/mangezmieux-backend/internal/acl/sql/sql_role.go b/mangezmieux-backend/internal/acl/sql/sql_role.go new file mode 100644 index 0000000..0fd07a1 --- /dev/null +++ b/mangezmieux-backend/internal/acl/sql/sql_role.go @@ -0,0 +1,121 @@ +package sql + +import ( + "database/sql" + "errors" + "github.com/google/uuid" + "github.com/lib/pq" + "mangezmieux-backend/internal/acl/model" + model2 "mangezmieux-backend/internal/model" + "mangezmieux-backend/internal/postgres" +) + +func (sqlDAO dao) GetRole(id uuid.UUID) (*model.Role, error) { + q := ` + SELECT id, name, creation_date, last_update_date + FROM mangezmieux.role r + WHERE r.id = $1 + ` + row := sqlDAO.client.QueryRow(q, id.String()) + + role := &model.Role{} + err := row.Scan(&role.Id, &role.Name, &role.CreationDate, &role.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + return role, nil +} + +func (sqlDAO dao) GetRoleByName(name string) (*model.Role, error) { + q := ` + SELECT id, name, creation_date, last_update_date + FROM mangezmieux.role r + WHERE r.name = $1 + ` + row := sqlDAO.client.QueryRow(q, name) + + role := &model.Role{} + err := row.Scan(&role.Id, &role.Name, &role.CreationDate, &role.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + return role, nil +} + +func (sqlDAO dao) AddRole(roleName string, metadata model2.Metadata) (*model.Role, error) { + var id uuid.UUID + q := ` + INSERT INTO mangezmieux.role + (name, creation_date, creation_user) + VALUES + ($1,$2,$3) + RETURNING + id` + + err := sqlDAO.client.QueryRow(q, roleName, metadata.CreationDate, metadata.CreationUser).Scan(&id) + + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + + role, err := sqlDAO.GetRole(id) + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + + return role, nil +} + +func (sqlDAO dao) DeleteRole(id uuid.UUID) error { + q := ` + DELETE FROM mangezmieux.role + WHERE id = $1 + ` + + _, err := sqlDAO.client.Exec(q, id.String()) + var errPq *pq.Error + if errors.As(err, &errPq) { + return postgres.HandlePgError(errPq) + } + return err +} + +func (sqlDAO dao) GetAllRole() ([]*model.Role, error) { + q := ` + SELECT id, name, creation_date, last_update_date + FROM mangezmieux.role r + ` + rows, err := sqlDAO.client.Query(q) + if err != nil { + return nil, err + } + + if rows.Err() != nil { + return nil, rows.Err() + } + + defer rows.Close() + roles := make([]*model.Role, 0) + for rows.Next() { + role := &model.Role{} + err := rows.Scan(&role.Id, &role.Name, &role.CreationDate, &role.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + roles = append(roles, role) + } + return roles, nil +} diff --git a/mangezmieux-backend/internal/acl/sql/sql_role_verb_resource.go b/mangezmieux-backend/internal/acl/sql/sql_role_verb_resource.go new file mode 100644 index 0000000..7779bc8 --- /dev/null +++ b/mangezmieux-backend/internal/acl/sql/sql_role_verb_resource.go @@ -0,0 +1,191 @@ +package sql + +import ( + "database/sql" + "errors" + "mangezmieux-backend/internal/acl/model" + model2 "mangezmieux-backend/internal/model" + "mangezmieux-backend/internal/postgres" + + "github.com/google/uuid" + "github.com/lib/pq" +) + +func (sqlDAO dao) GetRoleVerbResource(id uuid.UUID) (*model.RoleVerbResource, error) { + q := ` + SELECT id, role_id, verb, resource_id, creation_date, last_update_date + FROM mangezmieux.role_verb_resource r + WHERE r.id = $1 + ` + row := sqlDAO.client.QueryRow(q, id.String()) + + roleVerbResource := &model.RoleVerbResource{} + err := row.Scan(&roleVerbResource.Id, &roleVerbResource.RoleId, &roleVerbResource.Verb, &roleVerbResource.ResourceId, &roleVerbResource.CreationDate, &roleVerbResource.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + return roleVerbResource, nil +} + +func (sqlDAO dao) GetRoleVerbResourceByRoleResourceAndVerb(roleId, resourceId uuid.UUID, verb string) (*model.RoleVerbResource, error) { + q := ` + SELECT id, role_id, verb, resource_id, creation_date, last_update_date + FROM mangezmieux.role_verb_resource r + WHERE r.role_id = $1 + AND r.resource_id = $2 + AND r.verb = $3 + ` + row := sqlDAO.client.QueryRow(q, roleId.String(), resourceId.String(), verb) + + roleVerbResource := &model.RoleVerbResource{} + err := row.Scan(&roleVerbResource.Id, &roleVerbResource.RoleId, &roleVerbResource.Verb, &roleVerbResource.ResourceId, &roleVerbResource.CreationDate, &roleVerbResource.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + return roleVerbResource, nil +} + +func (sqlDAO dao) GetRoleVerbResourceByResourceAndVerb(resourceId uuid.UUID, verb string) ([]*model.RoleVerbResource, error) { + q := ` + SELECT id, role_id, verb, resource_id, creation_date, last_update_date + FROM mangezmieux.role_verb_resource r + WHERE r.resource_id = $1 + AND r.verb = $2 + ` + rows, err := sqlDAO.client.Query(q, resourceId.String(), verb) + if err != nil { + return nil, err + } + + if rows.Err() != nil { + return nil, rows.Err() + } + + defer rows.Close() + roleVerResources := make([]*model.RoleVerbResource, 0) + for rows.Next() { + roleVerbResource := &model.RoleVerbResource{} + err := rows.Scan(&roleVerbResource.Id, &roleVerbResource.RoleId, &roleVerbResource.Verb, &roleVerbResource.ResourceId, &roleVerbResource.CreationDate, &roleVerbResource.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + roleVerResources = append(roleVerResources, roleVerbResource) + } + return roleVerResources, nil +} + +func (sqlDAO dao) GetRoleVerbResourceByRoleResource(roleId, resourceId uuid.UUID) ([]*model.RoleVerbResource, error) { + q := ` + SELECT id, role_id, verb, resource_id, creation_date, last_update_date + FROM mangezmieux.role_verb_resource r + WHERE r.role_id = $1 + AND r.resource_id = $2 + ` + rows, err := sqlDAO.client.Query(q, roleId.String(), resourceId.String()) + if err != nil { + return nil, err + } + + if rows.Err() != nil { + return nil, rows.Err() + } + + defer rows.Close() + roleVerResources := make([]*model.RoleVerbResource, 0) + for rows.Next() { + roleVerbResource := &model.RoleVerbResource{} + err := rows.Scan(&roleVerbResource.Id, &roleVerbResource.RoleId, &roleVerbResource.Verb, &roleVerbResource.ResourceId, &roleVerbResource.CreationDate, &roleVerbResource.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + roleVerResources = append(roleVerResources, roleVerbResource) + } + return roleVerResources, nil +} + +func (sqlDAO dao) GetRoleVerbResourceByRole(roleId uuid.UUID) ([]*model.RoleVerbResource, error) { + q := ` + SELECT id, role_id, verb, resource_id, creation_date, last_update_date + FROM mangezmieux.role_verb_resource r + WHERE r.role_id = $1 + ` + rows, err := sqlDAO.client.Query(q, roleId.String()) + if err != nil { + return nil, err + } + + if rows.Err() != nil { + return nil, rows.Err() + } + + defer rows.Close() + roleVerResources := make([]*model.RoleVerbResource, 0) + for rows.Next() { + roleVerbResource := &model.RoleVerbResource{} + err := rows.Scan(&roleVerbResource.Id, &roleVerbResource.RoleId, &roleVerbResource.Verb, &roleVerbResource.ResourceId, &roleVerbResource.CreationDate, &roleVerbResource.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + roleVerResources = append(roleVerResources, roleVerbResource) + } + return roleVerResources, nil +} + +func (sqlDAO dao) AddRoleVerbResource(roleId, resourceId uuid.UUID, verb string, metadata model2.Metadata) (*model.RoleVerbResource, error) { + var Id uuid.UUID + q := ` + INSERT INTO mangezmieux.role_verb_resource + (role_id, verb, resource_id, creation_date, creation_user) + VALUES + ($1,$2,$3,$4,$5) + RETURNING + Id` + + err := sqlDAO.client.QueryRow(q, roleId.String(), verb, resourceId.String(), metadata.CreationDate, metadata.CreationUser).Scan(&Id) + + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + + roleVerbResource, err := sqlDAO.GetRoleVerbResource(Id) + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + + return roleVerbResource, nil +} + +func (sqlDAO dao) DeleteRoleVerbResource(id uuid.UUID) error { + q := ` + DELETE FROM mangezmieux.role_verb_resource + WHERE Id = $1 + ` + + _, err := sqlDAO.client.Exec(q, id.String()) + var errPq *pq.Error + if errors.As(err, &errPq) { + return postgres.HandlePgError(errPq) + } + return err +} diff --git a/mangezmieux-backend/internal/acl/sql/sql_user_role.go b/mangezmieux-backend/internal/acl/sql/sql_user_role.go new file mode 100644 index 0000000..dd98bd4 --- /dev/null +++ b/mangezmieux-backend/internal/acl/sql/sql_user_role.go @@ -0,0 +1,161 @@ +package sql + +import ( + "database/sql" + "errors" + "mangezmieux-backend/internal/acl/model" + model2 "mangezmieux-backend/internal/model" + "mangezmieux-backend/internal/postgres" + + "github.com/google/uuid" + "github.com/lib/pq" +) + +func (sqlDAO dao) GetUserRole(id uuid.UUID) (*model.UserRole, error) { + q := ` + SELECT id, role_id, user_id, creation_date, last_update_date + FROM mangezmieux.user_role r + WHERE r.id = $1 + ` + row := sqlDAO.client.QueryRow(q, id.String()) + + userRole := &model.UserRole{} + err := row.Scan(&userRole.Id, &userRole.RoleId, &userRole.UserId, &userRole.CreationDate, &userRole.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + return userRole, nil +} + +func (sqlDAO dao) GetUserRoleByUserAndRole(userId, roleId uuid.UUID) (*model.UserRole, error) { + q := ` + SELECT id, role_id, user_id, creation_date, last_update_date + FROM mangezmieux.user_role r + WHERE r.role_id = $1 + AND r.user_id = $2 + ` + row := sqlDAO.client.QueryRow(q, roleId.String(), userId.String()) + + userRole := &model.UserRole{} + err := row.Scan(&userRole.Id, &userRole.RoleId, &userRole.UserId, &userRole.CreationDate, &userRole.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + return userRole, nil +} + +func (sqlDAO dao) GetUserRoleByUser(userId uuid.UUID) ([]*model.UserRole, error) { + q := ` + SELECT id, role_id, user_id, creation_date, last_update_date + FROM mangezmieux.user_role r + WHERE r.user_id = $1 + ` + rows, err := sqlDAO.client.Query(q, userId.String()) + if errors.Is(err, sql.ErrNoRows) { + return make([]*model.UserRole, 0), nil + } + var errPq *pq.Error + if errors.As(err, &errPq) { + + return nil, postgres.HandlePgError(errPq) + } + + if rows.Err() != nil { + return nil, rows.Err() + } + + defer rows.Close() + userRoles := make([]*model.UserRole, 0) + for rows.Next() { + userRole := &model.UserRole{} + err := rows.Scan(&userRole.Id, &userRole.RoleId, &userRole.UserId, &userRole.CreationDate, &userRole.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + userRoles = append(userRoles, userRole) + } + return userRoles, nil +} + +func (sqlDAO dao) GetUserRoleByRole(role uuid.UUID) ([]*model.UserRole, error) { + q := ` + SELECT id, role_id, user_id, creation_date, last_update_date + FROM mangezmieux.user_role r + WHERE r.role_id = $1 + ` + rows, err := sqlDAO.client.Query(q, role.String()) + if err != nil { + return nil, err + } + + if rows.Err() != nil { + return nil, rows.Err() + } + + defer rows.Close() + userRoles := make([]*model.UserRole, 0) + for rows.Next() { + userRole := &model.UserRole{} + err := rows.Scan(&userRole.Id, &userRole.RoleId, &userRole.UserId, &userRole.CreationDate, &userRole.LastUpdateDate) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + userRoles = append(userRoles, userRole) + } + return userRoles, nil +} + +func (sqlDAO dao) AddUserRole(userId, roleId uuid.UUID, metadata model2.Metadata) (*model.UserRole, error) { + var Id uuid.UUID + q := ` + INSERT INTO mangezmieux.user_role + (user_id, role_id, creation_date, creation_user) + VALUES + ($1,$2,$3,$4) + RETURNING + Id` + + err := sqlDAO.client.QueryRow(q, userId.String(), roleId.String(), metadata.CreationDate, metadata.CreationUser).Scan(&Id) + + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + + userRole, err := sqlDAO.GetUserRole(Id) + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + + return userRole, nil +} + +func (sqlDAO dao) DeleteUserRole(id uuid.UUID) error { + q := ` + DELETE FROM mangezmieux.user_role + WHERE Id = $1 + ` + + _, err := sqlDAO.client.Exec(q, id.String()) + var errPq *pq.Error + if errors.As(err, &errPq) { + return postgres.HandlePgError(errPq) + } + return err +} diff --git a/mangezmieux-backend/internal/ginserver/headers.go b/mangezmieux-backend/internal/ginserver/headers.go new file mode 100755 index 0000000..e9c5a6e --- /dev/null +++ b/mangezmieux-backend/internal/ginserver/headers.go @@ -0,0 +1,8 @@ +package ginserver + +const ( + HeaderNameContentType = "content-type" + HeaderNameCorrelationID = "correlationID" + + HeaderValueApplicationJSONUTF8 = "application/json; charset=UTF-8" +) diff --git a/mangezmieux-backend/internal/ginserver/logger.go b/mangezmieux-backend/internal/ginserver/logger.go new file mode 100644 index 0000000..6e0defa --- /dev/null +++ b/mangezmieux-backend/internal/ginserver/logger.go @@ -0,0 +1,70 @@ +package ginserver + +import ( + "mangezmieux-backend/internal/logger" + "math/rand" + "time" + + "github.com/gin-gonic/gin" +) + +const ( + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return string(b) +} + +func GetLoggerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + correlationID := c.Request.Header.Get(HeaderNameCorrelationID) + if correlationID == "" { + correlationID = randStringBytesMaskImprSrc(30) + c.Writer.Header().Set(HeaderNameCorrelationID, correlationID) + } + + logEntry := logger.GetLogger().WithField(HeaderNameCorrelationID, correlationID) + + c.Set(logger.ContextKeyLogger, logEntry) + } +} + +func GetHTTPLoggerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + + logger.GetLoggerFromCtx(c). + WithField("method", c.Request.Method). + WithField("url", c.Request.RequestURI). + WithField("from", c.ClientIP()). + Info("start handling HTTP request") + + c.Next() + d := time.Since(start) + + logger.GetLoggerFromCtx(c). + WithField("status", c.Writer.Status()). + WithField("duration", d.String()). + Info("end handling HTTP request") + } +} diff --git a/mangezmieux-backend/internal/ginserver/setup.go b/mangezmieux-backend/internal/ginserver/setup.go new file mode 100644 index 0000000..f32d44f --- /dev/null +++ b/mangezmieux-backend/internal/ginserver/setup.go @@ -0,0 +1,45 @@ +package ginserver + +import ( + "mangezmieux-backend/internal/injector" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +var ( + routerInjectorKey = "ROUTER" + + SecuredRouterInjectorKey = "SECURED_ROUTER" + UnsecuredRouterInjectorKey = "UNSECURED_ROUTER" +) + +func Setup(inj *injector.Injector) { + gin.SetMode(gin.ReleaseMode) + + router := gin.New() + router.HandleMethodNotAllowed = true + + router.Use(cors.New(cors.Config{ + AllowOrigins: []string{"http://localhost:3000/", "http://localhost:3000"}, + 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("") + inj.Set(UnsecuredRouterInjectorKey, public) + + authMiddleware := injector.Get[gin.HandlerFunc](inj, "AuthenticationMiddleware") + + securedUserRoute := public.Group("/api/v1") + securedUserRoute.Use(authMiddleware) + + inj.Set(SecuredRouterInjectorKey, securedUserRoute) + inj.Set(routerInjectorKey, router) +} diff --git a/mangezmieux-backend/internal/ginserver/start.go b/mangezmieux-backend/internal/ginserver/start.go new file mode 100644 index 0000000..3d85126 --- /dev/null +++ b/mangezmieux-backend/internal/ginserver/start.go @@ -0,0 +1,41 @@ +package ginserver + +import ( + "errors" + "log" + "mangezmieux-backend/internal/injector" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" +) + +func Start(inj *injector.Injector, port string) { + router := injector.Get[*gin.Engine](inj, routerInjectorKey) + + srv := &http.Server{ + Addr: ":" + port, + Handler: router, + ReadHeaderTimeout: 4 * time.Second, + } + + go func() { + // service connections + if err := srv.ListenAndServe(); err != nil && !errors.Is(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, 1) + // 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 ...") +} diff --git a/mangezmieux-backend/internal/godog/model.go b/mangezmieux-backend/internal/godog/model.go new file mode 100644 index 0000000..d37d789 --- /dev/null +++ b/mangezmieux-backend/internal/godog/model.go @@ -0,0 +1,18 @@ +package godog + +import "context" + +type TestCenter interface { + GetFunctionalityContext() any +} +type genericTestCenter struct { + testCenter TestCenter +} + +type ResourceHandler interface { + Create(ctx context.Context, val string) (context.Context, error) + Read(ctx context.Context, val string) (context.Context, error) + Update(ctx context.Context, val string) (context.Context, error) + Patch(ctx context.Context, val string) (context.Context, error) + Delete(ctx context.Context, val string) (context.Context, error) +} diff --git a/mangezmieux-backend/internal/godog/resources.go b/mangezmieux-backend/internal/godog/resources.go new file mode 100644 index 0000000..2c5fcaf --- /dev/null +++ b/mangezmieux-backend/internal/godog/resources.go @@ -0,0 +1,23 @@ +package godog + +import ( + "fmt" + "log" +) + +var resourcesHandler = map[string]ResourceHandler{} + +func RegisterResourceHandler(resourceName string, handler ResourceHandler) { + if _, ok := resourcesHandler[resourceName]; ok { + log.Fatalf("Can't add an already existing handler for resource %s", resourceName) + return + } + resourcesHandler[resourceName] = handler +} + +func GetResourceHandler(resourceName string) ResourceHandler { + if val, ok := resourcesHandler[resourceName]; ok { + return val + } + panic(fmt.Sprintf("no handler found for resource type %s", resourceName)) +} diff --git a/mangezmieux-backend/internal/godog/sentence.go b/mangezmieux-backend/internal/godog/sentence.go new file mode 100644 index 0000000..d7c8091 --- /dev/null +++ b/mangezmieux-backend/internal/godog/sentence.go @@ -0,0 +1,92 @@ +package godog + +import ( + "context" + "encoding/json" + "fmt" + "github.com/cucumber/godog" + "github.com/ohler55/ojg/jp" + "net/http/httptest" +) + +func (gtc *genericTestCenter) theResourceIsCreated(ctx context.Context) (context.Context, error) { + return gtc.theResponseHasStatus(ctx, 201) +} + +func (gtc *genericTestCenter) theResourceAlreadyExists(ctx context.Context) (context.Context, error) { + return gtc.theResponseHasStatus(ctx, 409) +} + +func (gtc *genericTestCenter) theUserMissRight(ctx context.Context) (context.Context, error) { + return gtc.theResponseHasStatus(ctx, 403) +} + +func (gtc *genericTestCenter) badRequest(ctx context.Context) (context.Context, error) { + return gtc.theResponseHasStatus(ctx, 400) +} + +func (gtc *genericTestCenter) forbiddenRequest(ctx context.Context) (context.Context, error) { + return gtc.theResponseHasStatus(ctx, 403) +} + +func (gtc *genericTestCenter) theResponseHasStatus(ctx context.Context, status int) (context.Context, error) { + httpRecorder := ctx.Value("recorder").(*httptest.ResponseRecorder) + if httpRecorder.Code != status { + return ctx, fmt.Errorf("got code %d with body %s", httpRecorder.Code, httpRecorder.Body.String()) + } + + ctx = context.WithValue(ctx, "OBJECT_RESPONSE", httpRecorder.Body.String()) + return ctx, nil +} + +func (gtc *genericTestCenter) theFieldHasValue(ctx context.Context, fieldName, value string) (context.Context, error) { + compiledPath, err := jp.ParseString(fieldName) + body := ctx.Value("OBJECT_RESPONSE").(string) + if err != nil { + return ctx, err + } + var resourceAsMap interface{} + err = json.Unmarshal([]byte(body), &resourceAsMap) + if err != nil { + return ctx, err + } + + datas := compiledPath.Get(resourceAsMap) + if len(datas) != 1 { + return ctx, fmt.Errorf("Found %v data. Expected only one", len(datas)) + } + if datas[0] != value { + return ctx, fmt.Errorf("The field %s has value %s . Expected %s", fieldName, datas[0], value) + } + return ctx, nil +} + +func (gtc *genericTestCenter) resourceDoesntExist(ctx context.Context, resourceType string, resourceName string) (context.Context, error) { + handler := GetResourceHandler(resourceType) + ctx, _ = handler.Delete(ctx, resourceName) + return ctx, nil +} + +func (gtc *genericTestCenter) theUserCreatesAResourcesWithTheFollowingData(ctx context.Context, resourceType string, content *godog.DocString) (context.Context, error) { + handler := GetResourceHandler(resourceType) + ctx, _ = handler.Create(ctx, content.Content) + return ctx, nil +} + +func (gtc *genericTestCenter) theUserPatchesAResourcesWithTheFollowingData(ctx context.Context, resourceType string, content *godog.DocString) (context.Context, error) { + handler := GetResourceHandler(resourceType) + ctx, _ = handler.Patch(ctx, content.Content) + return ctx, nil +} + +func (gtc *genericTestCenter) theUserUpdatesAResourcesWithTheFollowingData(ctx context.Context, resourceType string, content *godog.DocString) (context.Context, error) { + handler := GetResourceHandler(resourceType) + ctx, _ = handler.Update(ctx, content.Content) + return ctx, nil +} + +func (gtc *genericTestCenter) theResourceExistWithTheFollowingData(ctx context.Context, resourceType string, content *godog.DocString) (context.Context, error) { + handler := GetResourceHandler(resourceType) + ctx, _ = handler.Create(ctx, content.Content) + return ctx, nil +} diff --git a/mangezmieux-backend/internal/godog/setup.go b/mangezmieux-backend/internal/godog/setup.go new file mode 100644 index 0000000..2318d54 --- /dev/null +++ b/mangezmieux-backend/internal/godog/setup.go @@ -0,0 +1,20 @@ +package godog + +import cucumber "github.com/cucumber/godog" + +func Setup(ctx *cucumber.ScenarioContext, testCenter TestCenter) { + gtc := genericTestCenter{testCenter: testCenter} + ctx.Step(`^the resource is created`, gtc.theResourceIsCreated) + ctx.Step(`^the response indicates that the ([^\s]+) already exists`, gtc.theResourceAlreadyExists) + ctx.Step(`^the response indicates that the user doesn't have right`, gtc.theUserMissRight) + ctx.Step(`^the response indicates that this a bad request`, gtc.badRequest) + ctx.Step(`^the response indicates that this a forbidden request`, gtc.forbiddenRequest) + ctx.Step(`^the response has status (\d+)$`, gtc.theResponseHasStatus) + ctx.Step(`^the ([^\s]+) ([^\s]+) doesn\'t exist yet`, gtc.resourceDoesntExist) + ctx.Step(`^the field ([^\s]+) is (\d+)$`, gtc.theFieldHasValue) + ctx.Step(`^the field ([^\s]+) has value "([^"]*)"$`, gtc.theFieldHasValue) + ctx.Step(`^the user create a[n]* ([^\s]+) with the following data:$`, gtc.theUserCreatesAResourcesWithTheFollowingData) + ctx.Step(`^the user update a[n]* ([^\s]+) with the following data:$`, gtc.theUserUpdatesAResourcesWithTheFollowingData) + ctx.Step(`^the user patch a[n]* ([^\s]+) with the following data:$`, gtc.theUserUpdatesAResourcesWithTheFollowingData) + ctx.Step(`^the ([^\s]+) exists with the following data:$`, gtc.theResourceExistWithTheFollowingData) +} diff --git a/mangezmieux-backend/internal/health/handler.go b/mangezmieux-backend/internal/health/handler.go new file mode 100644 index 0000000..c8d31ca --- /dev/null +++ b/mangezmieux-backend/internal/health/handler.go @@ -0,0 +1,27 @@ +package health + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// @openapi:path +// /_health: +// +// get: +// tags: +// - "Monitoring" +// summary: Health check +// description: Health check +// responses: +// 200: +// description: "Health response" +// content: +// application/json: +// schema: +// $ref: "#/components/schemas/Health" +func GetHealth(c *gin.Context) { + health := &Health{Alive: true} + c.JSON(http.StatusOK, health) +} diff --git a/mangezmieux-backend/internal/health/model.go b/mangezmieux-backend/internal/health/model.go new file mode 100644 index 0000000..0368ef3 --- /dev/null +++ b/mangezmieux-backend/internal/health/model.go @@ -0,0 +1,8 @@ +package health + +// Health struct +// @openapi:schema. +type Health struct { + Alive bool `json:"alive"` + Version string `json:"version"` +} diff --git a/mangezmieux-backend/internal/health/setup.go b/mangezmieux-backend/internal/health/setup.go new file mode 100644 index 0000000..21c84ab --- /dev/null +++ b/mangezmieux-backend/internal/health/setup.go @@ -0,0 +1,15 @@ +package health + +import ( + "mangezmieux-backend/internal/ginserver" + "mangezmieux-backend/internal/injector" + "net/http" + + "github.com/gin-gonic/gin" +) + +func Setup(inj *injector.Injector) { + publicRoute := injector.Get[*gin.RouterGroup](inj, ginserver.UnsecuredRouterInjectorKey) + + publicRoute.Handle(http.MethodGet, "/health", GetHealth) +} diff --git a/mangezmieux-backend/internal/injector/injector.go b/mangezmieux-backend/internal/injector/injector.go new file mode 100644 index 0000000..9fea2c1 --- /dev/null +++ b/mangezmieux-backend/internal/injector/injector.go @@ -0,0 +1,42 @@ +package injector + +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 (i *Injector) GetWithDefault(key string, defaultValue any) any { + val, ok := i.content[key] + if !ok { + return defaultValue + } + return val +} + +func Get[T any](i *Injector, key string) T { + return i.Get(key).(T) +} + +func GetWithDefault[T any](i *Injector, key string, defaultValue any) T { + return i.GetWithDefault(key, defaultValue).(T) +} + +func (i *Injector) Set(key string, content any) { + if i.content == nil { + i.content = map[string]any{} + } + + if _, ok := i.content[key]; ok { + panic(fmt.Sprintf("Key %s already have content", key)) + } + i.content[key] = content +} diff --git a/mangezmieux-backend/internal/jwt/service.go b/mangezmieux-backend/internal/jwt/service.go new file mode 100644 index 0000000..75254d5 --- /dev/null +++ b/mangezmieux-backend/internal/jwt/service.go @@ -0,0 +1,78 @@ +package jwt + +import ( + "mangezmieux-backend/internal/responses" + "time" + + jwtLib "github.com/golang-jwt/jwt/v5" +) + +type Service struct { + SecretKey string +} + +func NewService() *Service { + return &Service{SecretKey: "hard-coded-temp"} +} + +type Claims struct { + ID string `json:"username"` + jwtLib.RegisteredClaims +} + +func (s *Service) ValidateToken(token string) (*Claims, error) { + claims := &Claims{} + tkn, err := jwtLib.ParseWithClaims(token, claims, func(token *jwtLib.Token) (any, error) { + return []byte(s.SecretKey), nil + }) + if err != nil { + return nil, err + } + + if !tkn.Valid { + return nil, err + } + + return claims, nil +} + +func (s *Service) GenerateJWTToken(userId string) (string, error) { + expirationTime := time.Now().Add(10 * time.Minute) + + claims := &Claims{ + ID: userId, + RegisteredClaims: jwtLib.RegisteredClaims{ + // In JWT, the expiry time is expressed as unix milliseconds + ExpiresAt: jwtLib.NewNumericDate(expirationTime), + }, + } + token := jwtLib.NewWithClaims(jwtLib.SigningMethodHS256, claims) + // Create the JWT string + tokenString, err := token.SignedString([]byte(s.SecretKey)) + if err != nil { + return "", &responses.ErrInternalServer + } + return tokenString, nil +} + +func (s *Service) Refresh(oldToken string) (string, error) { + claims := &Claims{} + tkn, err := jwtLib.ParseWithClaims(oldToken, claims, func(token *jwtLib.Token) (any, error) { + return []byte(s.SecretKey), nil + }) + if err != nil { + return "", err + } + + if !tkn.Valid { + return "", err + } + + // Now, create a new token for the current use, with a renewed expiration time + expirationTime := time.Now().Add(10 * time.Minute) + claims.ExpiresAt = jwtLib.NewNumericDate(expirationTime) + token := jwtLib.NewWithClaims(jwtLib.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(s.SecretKey)) + return tokenString, nil + +} diff --git a/mangezmieux-backend/internal/jwt/setup.go b/mangezmieux-backend/internal/jwt/setup.go new file mode 100644 index 0000000..5e0cad6 --- /dev/null +++ b/mangezmieux-backend/internal/jwt/setup.go @@ -0,0 +1,16 @@ +package jwt + +import ( + "mangezmieux-backend/internal/injector" +) + +const JWTKey = "JWT" + +func Setup(inj *injector.Injector) { + + // build components + service := NewService() + + // register provided components + inj.Set(JWTKey, service) +} diff --git a/mangezmieux-backend/internal/logger/logger.go b/mangezmieux-backend/internal/logger/logger.go new file mode 100755 index 0000000..5ec4c7f --- /dev/null +++ b/mangezmieux-backend/internal/logger/logger.go @@ -0,0 +1,74 @@ +package logger + +import ( + "io" + "os" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +const ( + LogFormatText = "text" + LogFormatJSON = "json" + + ContextKeyLogger = "logger" +) + +var ( + logLevel = logrus.DebugLevel + logFormat logrus.Formatter = &logrus.TextFormatter{} + logOut io.Writer +) + +func InitLogger(ll, lf string) { + logLevel = parseLogrusLevel(ll) + logrus.SetLevel(logLevel) + + logFormat = parseLogrusFormat(lf) + logrus.SetFormatter(logFormat) + + logOut = os.Stdout + logrus.SetOutput(logOut) +} + +func GetLoggerFromCtx(c *gin.Context) *logrus.Entry { + if logger, ok := c.Get(ContextKeyLogger); ok { + logEntry, assertionOk := logger.(*logrus.Entry) + if assertionOk { + return logEntry + } + } + return logrus.NewEntry(GetLogger()) +} + +func GetLogger() *logrus.Logger { + logger := logrus.New() + logger.Formatter = logFormat + logger.Level = logLevel + logger.Out = logOut + return logger +} + +func parseLogrusLevel(logLevelStr string) logrus.Level { + logLevel, err := logrus.ParseLevel(logLevelStr) + if err != nil { + logrus.WithError(err).Errorf("error while parsing log level. %v is set as default.", logLevel) + logLevel = logrus.DebugLevel + } + return logLevel +} + +func parseLogrusFormat(logFormatStr string) logrus.Formatter { + var formatter logrus.Formatter + switch logFormatStr { + case LogFormatText: + formatter = &logrus.TextFormatter{ForceColors: true, FullTimestamp: true} + case LogFormatJSON: + formatter = &logrus.JSONFormatter{} + default: + logrus.Errorf("error while parsing log format. %v is set as default.", formatter) + formatter = &logrus.TextFormatter{ForceColors: true, FullTimestamp: true} + } + return formatter +} diff --git a/mangezmieux-backend/internal/middleware/introspect.go b/mangezmieux-backend/internal/middleware/introspect.go new file mode 100644 index 0000000..597edf0 --- /dev/null +++ b/mangezmieux-backend/internal/middleware/introspect.go @@ -0,0 +1,102 @@ +package middleware + +import ( + "context" + "errors" + model2 "mangezmieux-backend/internal/acl/model" + "mangezmieux-backend/internal/jwt" + "mangezmieux-backend/internal/logger" + "mangezmieux-backend/internal/responses" + "mangezmieux-backend/internal/users/model" + "strings" + + "github.com/gin-gonic/gin" +) + +const CtxUser = "user" +const CtxUserRight = "userRight" +const CtxRole = "role" +const CtxToken = "token" + +type IntrospectService interface { + Introspect(token string) (user *model.User, err error) + GetRole(ctx context.Context, user *model.User) (userRight *model2.UserRight, err error) + GetAllRole(ctx context.Context) ([]*model2.Role, error) +} + +func GetAuthenticationMiddleware(introspectService IntrospectService, jwtService *jwt.Service) gin.HandlerFunc { + + return func(c *gin.Context) { + token, err := getTokenFromGinCtx(c) + c.Set(CtxToken, token) + + ctx := c.Request.Context() + ctx = context.WithValue(ctx, CtxToken, token) + if err != nil { + logger.GetLogger().WithError(err).Debug("no token found") + responses.JSONErrorWithMessage(c.Writer, responses.ErrBadRequestFormat, err.Error()) + c.Abort() + return + } + + _, err = jwtService.ValidateToken(token) + if err != nil { + logger.GetLogger().WithError(err).Debug("error during token validation") + responses.JSONErrorWithMessage(c.Writer, responses.ErrBadRequestFormat, err.Error()) + c.Abort() + return + } + user, err := introspectService.Introspect(token) + if err != nil { + logger.GetLogger().WithError(err).Debug("error during introspect") + responses.JSONErrorWithMessage(c.Writer, responses.ErrBadRequestFormat, err.Error()) + c.Abort() + return + } + c.Set(CtxUser, user) + + userRight, err := introspectService.GetRole(ctx, user) + if err != nil { + logger.GetLogger().WithError(err).Debug("error during getting role for user") + responses.JSONErrorWithMessage(c.Writer, responses.ErrBadRequestFormat, err.Error()) + c.Abort() + return + } + c.Set(CtxUserRight, userRight) + + roles, err := introspectService.GetAllRole(ctx) + if err != nil { + logger.GetLogger().WithError(err).Debug("error during getting role map") + responses.JSONErrorWithMessage(c.Writer, responses.ErrBadRequestFormat, err.Error()) + c.Abort() + return + } + c.Set(CtxRole, roles) + + c.Next() + } +} + +// getTokenFromGinCtx allow to get the access token of the request in the Authorization request header. +// It will split the header and remove the Bearer part to extract only the token. +func getTokenFromGinCtx(c *gin.Context) (string, error) { + auth := c.GetHeader("Authorization") + if auth != "" { + authSplitted := strings.SplitN(auth, " ", 2) + if len(authSplitted) != 2 { + return "", errors.New("malformed authorization header") + } + + if strings.ToUpper(authSplitted[0]) != strings.ToUpper("Bearer") && strings.ToUpper(authSplitted[0]) != strings.ToUpper("JWT") { + return "", errors.New("unsupported authentication scheme") + } + + return authSplitted[1], nil + } + + if cookie, err := c.Cookie("token"); err == nil { + return cookie, nil + } + + return "", errors.New("no token found in the request") +} diff --git a/mangezmieux-backend/internal/middleware/setup.go b/mangezmieux-backend/internal/middleware/setup.go new file mode 100644 index 0000000..86ce1bb --- /dev/null +++ b/mangezmieux-backend/internal/middleware/setup.go @@ -0,0 +1,16 @@ +package middleware + +import ( + "mangezmieux-backend/internal/injector" + "mangezmieux-backend/internal/jwt" +) + +const AuthenticationMiddlewareKey = "AuthenticationMiddleware" +const IntrospectServiceKey = "AuthCli" + +func Setup(inj *injector.Injector) { + + jwtService := injector.Get[*jwt.Service](inj, jwt.JWTKey) + introspectService := injector.Get[IntrospectService](inj, IntrospectServiceKey) + inj.Set(AuthenticationMiddlewareKey, GetAuthenticationMiddleware(introspectService, jwtService)) +} diff --git a/mangezmieux-backend/internal/model/metadata.go b/mangezmieux-backend/internal/model/metadata.go new file mode 100644 index 0000000..46947dc --- /dev/null +++ b/mangezmieux-backend/internal/model/metadata.go @@ -0,0 +1,10 @@ +package model + +import "time" + +type Metadata struct { + CreationDate time.Time `json:"creation_date"` + LastUpdateDate time.Time `json:"last_update_date"` + CreationUser string `json:"creation_user"` + LastUpdateUser string `json:"last_update_user"` +} diff --git a/mangezmieux-backend/internal/postgres/database_error.go b/mangezmieux-backend/internal/postgres/database_error.go new file mode 100755 index 0000000..74b13dd --- /dev/null +++ b/mangezmieux-backend/internal/postgres/database_error.go @@ -0,0 +1,32 @@ +package postgres + +import ( + "fmt" +) + +type Type int + +const ( + ErrTypeNotFound Type = iota + ErrTypeDuplicate + ErrTypeForeignKeyViolation +) + +type Error struct { + Cause error + Type Type +} + +func NewDAOError(t Type, cause error) error { + return &Error{ + Type: t, + Cause: cause, + } +} + +func (e *Error) Error() string { + if e.Cause != nil { + return fmt.Sprintf("Type %d: %s", e.Type, e.Cause.Error()) + } + return fmt.Sprintf("Type %d: no cause given", e.Type) +} diff --git a/mangezmieux-backend/internal/postgres/database_postgresql.go b/mangezmieux-backend/internal/postgres/database_postgresql.go new file mode 100644 index 0000000..6822cab --- /dev/null +++ b/mangezmieux-backend/internal/postgres/database_postgresql.go @@ -0,0 +1,35 @@ +package postgres + +import ( + "database/sql" + "github.com/lib/pq" + "mangezmieux-backend/internal/logger" +) + +const ( + pgCodeUniqueViolation = "23505" + pgCodeForeingKeyViolation = "23503" +) + +func HandlePgError(e *pq.Error) error { + if e.Code == pgCodeUniqueViolation { + return NewDAOError(ErrTypeDuplicate, e) + } + + if e.Code == pgCodeForeingKeyViolation { + return NewDAOError(ErrTypeForeignKeyViolation, e) + } + return e +} + +func NewDatabasePostgreSQL(connectionURI string) *sql.DB { + db, err := sql.Open("postgres", connectionURI) + if err != nil { + logger.GetLogger().WithError(err).Fatal("Unable to get a connection to the postgres db") + } + err = db.Ping() + if err != nil { + logger.GetLogger().WithError(err).Fatal("Unable to ping the postgres db") + } + return db +} diff --git a/mangezmieux-backend/internal/postgres/setup.go b/mangezmieux-backend/internal/postgres/setup.go new file mode 100644 index 0000000..b2ced79 --- /dev/null +++ b/mangezmieux-backend/internal/postgres/setup.go @@ -0,0 +1,11 @@ +package postgres + +import "mangezmieux-backend/internal/injector" + +const DatabaseKey = "POSTGRES" + +func Setup(inj *injector.Injector, connectionURI string) { + client := NewDatabasePostgreSQL(connectionURI) + + inj.Set(DatabaseKey, client) +} diff --git a/mangezmieux-backend/internal/responses/error.go b/mangezmieux-backend/internal/responses/error.go new file mode 100644 index 0000000..aa65269 --- /dev/null +++ b/mangezmieux-backend/internal/responses/error.go @@ -0,0 +1,62 @@ +package responses + +import ( + "fmt" + "net/http" +) + +var ( + ErrBadRequestFormat = APIError{ + Type: "bad_format", + HTTPCode: http.StatusBadRequest, + Description: "unable to read request body, please check that the json is valid", + } + ErrDataValidation = APIError{ + Type: "data_validation", + HTTPCode: http.StatusBadRequest, + Description: "the data are not valid", + } + + ErrNotFound = APIError{ + Type: "not_found", + HTTPCode: http.StatusNotFound, + } + + ErrAlreadyExists = APIError{ + Type: "already_exists", + HTTPCode: http.StatusConflict, + } + + ErrUnauthorized = APIError{ + Type: "unauthorized", + HTTPCode: http.StatusUnauthorized, + } + + ErrForbidden = APIError{ + Type: "forbidden", + HTTPCode: http.StatusForbidden, + } + + ErrInternalServer = APIError{ + Type: "internal_server_error", + HTTPCode: http.StatusInternalServerError, + } +) + +type APIError struct { + HTTPCode int `json:"-"` + Type string `json:"error"` + Description string `json:"errorDescription"` + Details []FieldError `json:"errorDetails,omitempty"` + Headers map[string][]string `json:"-"` +} + +type FieldError struct { + Field string `json:"field"` + Constraint string `json:"constraint"` + Description string `json:"description"` +} + +func (e *APIError) Error() string { + return fmt.Sprintf("error : %d, %s, %s, %v", e.HTTPCode, e.Type, e.Description, e.Details) +} diff --git a/mangezmieux-backend/internal/responses/responses.go b/mangezmieux-backend/internal/responses/responses.go new file mode 100644 index 0000000..60656a1 --- /dev/null +++ b/mangezmieux-backend/internal/responses/responses.go @@ -0,0 +1,34 @@ +package responses + +import ( + "encoding/json" + "mangezmieux-backend/internal/ginserver" + "net/http" +) + +func JSON(w http.ResponseWriter, status int, data interface{}) { + w.Header().Set(ginserver.HeaderNameContentType, ginserver.HeaderValueApplicationJSONUTF8) + w.WriteHeader(status) + if data != nil { + err := json.NewEncoder(w).Encode(data) + if err != nil { + return + } + } +} + +func JSONError(w http.ResponseWriter, e APIError) { + if e.Headers != nil { + for k, headers := range e.Headers { + for _, headerValue := range headers { + w.Header().Add(k, headerValue) + } + } + } + JSON(w, e.HTTPCode, e) +} + +func JSONErrorWithMessage(w http.ResponseWriter, e APIError, message string) { + e.Description = message + JSONError(w, e) +} diff --git a/mangezmieux-backend/internal/users/handler.go b/mangezmieux-backend/internal/users/handler.go new file mode 100644 index 0000000..8e0a85a --- /dev/null +++ b/mangezmieux-backend/internal/users/handler.go @@ -0,0 +1,123 @@ +package users + +import ( + "errors" + "github.com/go-playground/validator/v10" + "mangezmieux-backend/internal/logger" + "mangezmieux-backend/internal/middleware" + "mangezmieux-backend/internal/responses" + "mangezmieux-backend/internal/users/model" + "mangezmieux-backend/internal/users/service" + + "net/http" + "strings" + + "github.com/gin-gonic/gin" + coreValidator "mangezmieux-backend/internal/validator" +) + +type Handler struct { + Service *service.Service + Validator *validator.Validate +} + +func NewHandler(service *service.Service, validator *validator.Validate) *Handler { + return &Handler{ + Service: service, + Validator: validator, + } +} + +func (h Handler) CreateUser(context *gin.Context) { + userEditable := model.UserEditable{} + if err := context.BindJSON(&userEditable); err != nil { + responses.JSONError(context.Writer, coreValidator.NewDataValidationAPIError(err)) + return + } + user, err := h.Service.CreateUser(&userEditable) + if err != nil { + logger.GetLogger().Error(err) + var apiError *responses.APIError + if errors.As(err, &apiError) { + responses.JSONError(context.Writer, *apiError) + return + } + responses.JSONErrorWithMessage(context.Writer, responses.ErrInternalServer, err.Error()) + return + } + responses.JSON(context.Writer, http.StatusCreated, user) +} + +func (h Handler) Login(context *gin.Context) { + userLoginRequest := model.UserLoginRequest{} + if err := context.BindJSON(&userLoginRequest); err != nil { + responses.JSONError(context.Writer, coreValidator.NewDataValidationAPIError(err)) + return + } + token, err := h.Service.Login(userLoginRequest) + if err != nil { + logger.GetLogger().Error(err) + var apiError *responses.APIError + if errors.As(err, &apiError) { + responses.JSONError(context.Writer, *apiError) + return + } + responses.JSONErrorWithMessage(context.Writer, responses.ErrInternalServer, err.Error()) + return + } + context.SetCookie("token", token, 10, "/", "localhost", true, false) + userLoginResponse := model.UserLoginResponse{ + AccessToken: token, + TokenType: "Bearer", + } + responses.JSON(context.Writer, http.StatusOK, userLoginResponse) +} + +func (h Handler) IntrospectToken(context *gin.Context) { + authorization := context.Request.Header.Get("Authorization") + splitToken := strings.Split(authorization, "Bearer ") + reqToken := splitToken[1] + user, err := h.Service.Introspect(reqToken) + if err != nil { + logger.GetLogger().Error(err) + var apiError *responses.APIError + if errors.As(err, &apiError) { + responses.JSONError(context.Writer, *apiError) + return + } + responses.JSONErrorWithMessage(context.Writer, responses.ErrInternalServer, err.Error()) + return + + } + responses.JSON(context.Writer, http.StatusOK, user) +} + +func (h Handler) RefreshToken(context *gin.Context) { + authorization := context.Request.Header.Get("Authorization") + splitToken := strings.Split(authorization, "Bearer ") + reqToken := splitToken[1] + + refreshedToken, err := h.Service.Refresh(reqToken) + if err != nil { + logger.GetLogger().Error(err) + var apiError *responses.APIError + if errors.As(err, &apiError) { + responses.JSONError(context.Writer, *apiError) + return + } + responses.JSONErrorWithMessage(context.Writer, responses.ErrInternalServer, err.Error()) + return + + } + context.SetCookie("token", refreshedToken, 10, "/", "localhost", true, false) +} + +func (h Handler) GetMe(context *gin.Context) { + usr, exists := context.Get(middleware.CtxUser) + if !exists { + responses.JSONErrorWithMessage(context.Writer, responses.ErrInternalServer, "User not found in context") + return + } + responses.JSON(context.Writer, http.StatusOK, usr) + +} diff --git a/mangezmieux-backend/internal/users/middleware.go b/mangezmieux-backend/internal/users/middleware.go new file mode 100644 index 0000000..c09e6c2 --- /dev/null +++ b/mangezmieux-backend/internal/users/middleware.go @@ -0,0 +1,44 @@ +package users + +import ( + "context" + "github.com/gin-gonic/gin" + model2 "mangezmieux-backend/internal/acl/model" + "mangezmieux-backend/internal/acl/service" + "mangezmieux-backend/internal/jwt" + "mangezmieux-backend/internal/middleware" + "mangezmieux-backend/internal/users/model" + service2 "mangezmieux-backend/internal/users/service" +) + +var AuthMiddleware = newMiddleware() + +type internalAuthMiddleware struct { + Service *service2.Service + RoleService service.Service + UserService service.Service +} + +func newMiddleware() *internalAuthMiddleware { + return &internalAuthMiddleware{} +} + +func (m *internalAuthMiddleware) GinMiddleware(jwtService *jwt.Service) gin.HandlerFunc { + return middleware.GetAuthenticationMiddleware(m, jwtService) +} + +// delegate useful for deferred binding (when the middleware is installed, GinMiddleware() is called, the service m.Service is not yet created :-( ) +// see cmd/app.go for deferred binding at the end. +func (m *internalAuthMiddleware) Introspect(token string) (*model.User, error) { + return m.Service.Introspect(token) +} + +// delegate useful for deferred binding (when the middleware is installed, GinMiddleware() is called, the service m.Service is not yet created :-( ) +// see cmd/app.go for deferred binding at the end. +func (m *internalAuthMiddleware) GetRole(ctx context.Context, user *model.User) (*model2.UserRight, error) { + return m.RoleService.GetRoleForCurrentUser(user) +} + +func (m *internalAuthMiddleware) GetAllRole(ctx context.Context) ([]*model2.Role, error) { + return m.RoleService.GetAllRole() +} diff --git a/mangezmieux-backend/internal/users/model/model.go b/mangezmieux-backend/internal/users/model/model.go new file mode 100644 index 0000000..efa13cd --- /dev/null +++ b/mangezmieux-backend/internal/users/model/model.go @@ -0,0 +1,31 @@ +package model + +import ( + "github.com/gofrs/uuid" + "time" +) + +type User struct { + ID *uuid.UUID `json:"ID"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt *time.Time `json:"updatedAt"` + UserEditable +} + +type UserEditable struct { + Firstname string `json:"first_name" binding:"required"` + Lastname string `json:"last_name" binding:"required"` + Email string `json:"email" binding:"required"` + Password string `json:"password" binding:"required"` +} + +type UserLoginRequest struct { + Email string `json:"email" binding:"required"` + Password string `json:"password" binding:"required"` +} + +type UserLoginResponse struct { + AccessToken string `json:"accessToken"` + TokenType string `json:"tokenType"` + ExpiresIn string `json:"expiresIn"` +} diff --git a/mangezmieux-backend/internal/users/service/service.go b/mangezmieux-backend/internal/users/service/service.go new file mode 100644 index 0000000..c53c013 --- /dev/null +++ b/mangezmieux-backend/internal/users/service/service.go @@ -0,0 +1,98 @@ +package service + +import ( + "mangezmieux-backend/internal/jwt" + "mangezmieux-backend/internal/responses" + "mangezmieux-backend/internal/users/model" + "mangezmieux-backend/internal/users/sql" + + "time" + + "golang.org/x/crypto/bcrypt" +) + +type Service struct { + dao sql.Dao + jwt *jwt.Service +} + +func NewService(dao sql.Dao, jwt *jwt.Service) *Service { + return &Service{dao: dao, jwt: jwt} +} + +func (s *Service) CreateUser(userEditable *model.UserEditable) (*model.User, error) { + now := time.Now() + user := &model.User{ + ID: nil, + CreatedAt: now, + UpdatedAt: &now, + UserEditable: model.UserEditable{ + Firstname: userEditable.Firstname, + Lastname: userEditable.Lastname, + Email: userEditable.Email, + Password: userEditable.Password, + }, + } + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(userEditable.Password), 8) + if err != nil { + return nil, &responses.ErrInternalServer + } + user.Password = string(hashedPassword) + err = s.dao.Create(user) + if err != nil { + return nil, err + } + user.Password = "" + return user, nil +} + +func (s *Service) Login(request model.UserLoginRequest) (string, error) { + user, err := s.dao.FindByMail(request.Email) + if err != nil { + return "", &responses.ErrUnauthorized + } + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(request.Password)) + if err != nil { + return "", &responses.ErrUnauthorized + } + return s.jwt.GenerateJWTToken(user.ID.String()) +} + +func (s *Service) Introspect(token string) (*model.User, error) { + claims, err := s.jwt.ValidateToken(token) + if err != nil { + return nil, err + } + + usr, err := s.dao.FindByID(claims.ID) + if err != nil { + return nil, err + } + + userModel := s.transformEntityToResponse(usr, false) + + return userModel, nil +} + +func (s *Service) Refresh(oldToken string) (string, error) { + return s.jwt.Refresh(oldToken) +} + +func (s *Service) transformEntityToResponse(user *model.User, withPassword bool) *model.User { + password := "" + if withPassword { + password = user.Password + } + + return &model.User{ + ID: user.ID, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + UserEditable: model.UserEditable{ + Firstname: user.Firstname, + Lastname: user.Lastname, + Email: user.Email, + Password: password, + }, + } +} diff --git a/mangezmieux-backend/internal/users/setup.go b/mangezmieux-backend/internal/users/setup.go new file mode 100644 index 0000000..57e1d5a --- /dev/null +++ b/mangezmieux-backend/internal/users/setup.go @@ -0,0 +1,42 @@ +package users + +import ( + "database/sql" + "mangezmieux-backend/internal/ginserver" + "mangezmieux-backend/internal/injector" + "mangezmieux-backend/internal/jwt" + "mangezmieux-backend/internal/postgres" + service2 "mangezmieux-backend/internal/users/service" + sql2 "mangezmieux-backend/internal/users/sql" + "mangezmieux-backend/internal/validator" + + "net/http" + + "github.com/gin-gonic/gin" + validatorv10 "github.com/go-playground/validator/v10" +) + +const ServiceKey = "UsersService" + +func Setup(inj *injector.Injector) { + publicRoute := injector.Get[*gin.RouterGroup](inj, ginserver.UnsecuredRouterInjectorKey) + validatorCli := injector.Get[*validatorv10.Validate](inj, validator.ValidatorInjectorKey) + jwtService := injector.Get[*jwt.Service](inj, jwt.JWTKey) + + client := injector.Get[*sql.DB](inj, postgres.DatabaseKey) + dao := sql2.NewDao(client) + + service := service2.NewService(dao, jwtService) + handler := NewHandler(service, validatorCli) + + inj.Set(ServiceKey, service) + + publicRoute.Handle(http.MethodPost, "/api/v1/users", handler.CreateUser) + publicRoute.Handle(http.MethodPost, "/oauth2/token", handler.Login) + publicRoute.Handle(http.MethodPost, "/oauth2/introspect", handler.IntrospectToken) + publicRoute.Handle(http.MethodPost, "/oauth2/refresh", handler.RefreshToken) + + securedRoute := injector.Get[*gin.RouterGroup](inj, ginserver.SecuredRouterInjectorKey) + securedRoute.Handle(http.MethodGet, "/users/me", handler.GetMe) + +} diff --git a/mangezmieux-backend/internal/users/sql/dao.go b/mangezmieux-backend/internal/users/sql/dao.go new file mode 100644 index 0000000..17cb15c --- /dev/null +++ b/mangezmieux-backend/internal/users/sql/dao.go @@ -0,0 +1,11 @@ +package sql + +import "mangezmieux-backend/internal/users/model" + +type Dao interface { + FindByMail(mail string) (*model.User, error) + Create(user *model.User) error + Delete(mail string) error + FindByMailAndPassword(mail string, password string) (*model.User, error) + FindByID(id string) (*model.User, error) +} diff --git a/mangezmieux-backend/internal/users/sql/sql.go b/mangezmieux-backend/internal/users/sql/sql.go new file mode 100644 index 0000000..eb598a2 --- /dev/null +++ b/mangezmieux-backend/internal/users/sql/sql.go @@ -0,0 +1,111 @@ +package sql + +import ( + "database/sql" + "errors" + "mangezmieux-backend/internal/postgres" + "mangezmieux-backend/internal/users/model" + + "github.com/lib/pq" +) + +type SQLDao struct { + client *sql.DB +} + +func NewDao(client *sql.DB) Dao { + return &SQLDao{client: client} +} + +func (sqlDAO *SQLDao) FindByMailAndPassword(mail string, password string) (*model.User, error) { + q := ` + SELECT u.ID, u.first_name, u.last_name, u.creation_date, u.last_update_date + FROM mangezmieux.user u + WHERE u.email = $1 AND u.password = $2 + ` + row := sqlDAO.client.QueryRow(q, mail, password) + + u := model.User{} + err := row.Scan(&u.Email, &u.Firstname, &u.Lastname, &u.CreatedAt, &u.UpdatedAt) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + return &u, err +} + +func (sqlDAO *SQLDao) FindByMail(mail string) (*model.User, error) { + q := ` + SELECT u.ID, u.email, u.first_name, u.last_name, u.creation_date, u.last_update_date, u.password + FROM mangezmieux.user u + WHERE u.email = $1 + ` + row := sqlDAO.client.QueryRow(q, mail) + + u := model.User{} + err := row.Scan(&u.ID, &u.Email, &u.Firstname, &u.Lastname, &u.CreatedAt, &u.UpdatedAt, &u.Password) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + return &u, err +} + +func (sqlDAO *SQLDao) FindByID(id string) (*model.User, error) { + q := ` + SELECT u.ID, u.email, u.first_name, u.last_name, u.creation_date, u.last_update_date + FROM mangezmieux.user u + WHERE u.id = $1 + ` + row := sqlDAO.client.QueryRow(q, id) + + u := model.User{} + err := row.Scan(&u.ID, &u.Email, &u.Firstname, &u.Lastname, &u.CreatedAt, &u.UpdatedAt) + var errPq *pq.Error + if errors.As(err, &errPq) { + return nil, postgres.HandlePgError(errPq) + } + if errors.Is(err, sql.ErrNoRows) { + return nil, postgres.NewDAOError(postgres.ErrTypeNotFound, err) + } + return &u, err +} + +func (sqlDAO *SQLDao) Create(user *model.User) error { + q := ` + INSERT INTO mangezmieux.user + (email, password, first_name, last_name, creation_date, last_update_date) + VALUES + ($1, $2, $3, $4, $5, $6) + RETURNING id, creation_date + ` + + err := sqlDAO.client. + QueryRow(q, user.Email, user.Password, user.Firstname, user.Lastname, user.CreatedAt, user.UpdatedAt). + Scan(&user.ID, &user.CreatedAt) + var errPq *pq.Error + if errors.As(err, &errPq) { + return postgres.HandlePgError(errPq) + } + return err +} + +func (sqlDAO *SQLDao) Delete(id string) error { + q := ` + DELETE FROM mangezmieux.user + WHERE id = $1 + ` + + _, err := sqlDAO.client.Exec(q, id) + var errPq *pq.Error + if errors.As(err, &errPq) { + return postgres.HandlePgError(errPq) + } + return err +} diff --git a/mangezmieux-backend/internal/validator/error.go b/mangezmieux-backend/internal/validator/error.go new file mode 100644 index 0000000..98d07ea --- /dev/null +++ b/mangezmieux-backend/internal/validator/error.go @@ -0,0 +1,51 @@ +package validator + +import ( + "errors" + "fmt" + "mangezmieux-backend/internal/logger" + "mangezmieux-backend/internal/responses" + "regexp" + "strings" + + validatorLib "github.com/go-playground/validator/v10" +) + +var regexpValidatorNamespacePrefix = regexp.MustCompile(`^\w+\.`) + +func NewDataValidationAPIError(err error) responses.APIError { + apiErr := responses.ErrDataValidation + if err != nil { + var invalidValidationErrror *validatorLib.InvalidValidationError + if errors.As(err, &invalidValidationErrror) { + logger.GetLogger().WithError(invalidValidationErrror).WithField("templateAPIErr", apiErr).Error("InvalidValidationError") + } else { + var validationErrors validatorLib.ValidationErrors + if errors.As(err, &validationErrors) { + for _, e := range validationErrors { + reason := e.Tag() + if _, ok := CustomValidators[e.Tag()]; ok { + reason = truncatingSprintf(CustomValidators[e.Tag()].Message, e.Param()) + } + + namespaceWithoutStructName := regexpValidatorNamespacePrefix.ReplaceAllString(e.Namespace(), "") + fe := responses.FieldError{ + Field: namespaceWithoutStructName, + Constraint: e.Tag(), + Description: reason, + } + apiErr.Details = append(apiErr.Details, fe) + } + } else { + apiErr.Description = err.Error() + } + } + } + return apiErr +} + +// truncatingSprintf is used as fmt.Sprintf but allow to truncate the additional parameters given when there is more parameters than %v in str. +func truncatingSprintf(str string, args ...interface{}) string { + n := strings.Count(str, "%v") + return fmt.Sprintf(str, args[:n]...) +} diff --git a/mangezmieux-backend/internal/validator/setup.go b/mangezmieux-backend/internal/validator/setup.go new file mode 100644 index 0000000..a59e028 --- /dev/null +++ b/mangezmieux-backend/internal/validator/setup.go @@ -0,0 +1,9 @@ +package validator + +import "mangezmieux-backend/internal/injector" + +const ValidatorInjectorKey = "VALIDATOR" + +func Setup(inj *injector.Injector) { + inj.Set(ValidatorInjectorKey, newValidator()) +} diff --git a/mangezmieux-backend/internal/validator/validator.go b/mangezmieux-backend/internal/validator/validator.go new file mode 100644 index 0000000..22abf0a --- /dev/null +++ b/mangezmieux-backend/internal/validator/validator.go @@ -0,0 +1,56 @@ +package validator + +import ( + "context" + "reflect" + "strings" + + validatorLib "github.com/go-playground/validator/v10" +) + +var CustomValidators = map[string]customValidator{ + "enum": { + Message: "This field should be in: %v", + Validator: validateEnum, + }, + "required": { + Message: "This field is required and cannot be empty", + }, +} + +type customValidator struct { + Message string + Validator validatorLib.FuncCtx +} + +func validateEnum(ctx context.Context, fl validatorLib.FieldLevel) bool { + for _, v := range strings.Split(fl.Param(), " ") { + if v == fl.Field().String() { + return true + } + } + return false +} + +func newValidator() *validatorLib.Validate { + va := validatorLib.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 CustomValidators { + if v.Validator != nil { + err := va.RegisterValidationCtx(k, v.Validator) + if err != nil { + return nil + } + } + } + + return va +} diff --git a/mangezmieux-backend/liquibase/changelogs/changelog-master.xml b/mangezmieux-backend/liquibase/changelogs/changelog-master.xml new file mode 100644 index 0000000..dae0484 --- /dev/null +++ b/mangezmieux-backend/liquibase/changelogs/changelog-master.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mangezmieux-backend/liquibase/changelogs/changesets/create-ingredient-table.xml b/mangezmieux-backend/liquibase/changelogs/changesets/create-ingredient-table.xml new file mode 100644 index 0000000..9aa733c --- /dev/null +++ b/mangezmieux-backend/liquibase/changelogs/changesets/create-ingredient-table.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mangezmieux-backend/liquibase/changelogs/changesets/create-recipe-table.xml b/mangezmieux-backend/liquibase/changelogs/changesets/create-recipe-table.xml new file mode 100644 index 0000000..87344e5 --- /dev/null +++ b/mangezmieux-backend/liquibase/changelogs/changesets/create-recipe-table.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mangezmieux-backend/liquibase/changelogs/changesets/create-resource-table.xml b/mangezmieux-backend/liquibase/changelogs/changesets/create-resource-table.xml new file mode 100644 index 0000000..559ac20 --- /dev/null +++ b/mangezmieux-backend/liquibase/changelogs/changesets/create-resource-table.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mangezmieux-backend/liquibase/changelogs/changesets/create-role-table.xml b/mangezmieux-backend/liquibase/changelogs/changesets/create-role-table.xml new file mode 100644 index 0000000..6ad67ab --- /dev/null +++ b/mangezmieux-backend/liquibase/changelogs/changesets/create-role-table.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mangezmieux-backend/liquibase/changelogs/changesets/create-role-verb-table.xml b/mangezmieux-backend/liquibase/changelogs/changesets/create-role-verb-table.xml new file mode 100644 index 0000000..a652a06 --- /dev/null +++ b/mangezmieux-backend/liquibase/changelogs/changesets/create-role-verb-table.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mangezmieux-backend/liquibase/changelogs/changesets/create-user-role-table.xml b/mangezmieux-backend/liquibase/changelogs/changesets/create-user-role-table.xml new file mode 100644 index 0000000..0d4d008 --- /dev/null +++ b/mangezmieux-backend/liquibase/changelogs/changesets/create-user-role-table.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mangezmieux-backend/liquibase/changelogs/changesets/create-user-table.xml b/mangezmieux-backend/liquibase/changelogs/changesets/create-user-table.xml new file mode 100644 index 0000000..9aa733c --- /dev/null +++ b/mangezmieux-backend/liquibase/changelogs/changesets/create-user-table.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mangezmieux-backend/liquibase/changelogs/changesets/insert-admin-role.xml b/mangezmieux-backend/liquibase/changelogs/changesets/insert-admin-role.xml new file mode 100644 index 0000000..512db14 --- /dev/null +++ b/mangezmieux-backend/liquibase/changelogs/changesets/insert-admin-role.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mangezmieux-backend/liquibase/changelogs/changesets/insert-admin-user.xml b/mangezmieux-backend/liquibase/changelogs/changesets/insert-admin-user.xml new file mode 100644 index 0000000..18f850d --- /dev/null +++ b/mangezmieux-backend/liquibase/changelogs/changesets/insert-admin-user.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/mangezmieux-backend/liquibase/changelogs/changesets/insert-mangezmieux-admin-role.xml b/mangezmieux-backend/liquibase/changelogs/changesets/insert-mangezmieux-admin-role.xml new file mode 100644 index 0000000..8c51ca8 --- /dev/null +++ b/mangezmieux-backend/liquibase/changelogs/changesets/insert-mangezmieux-admin-role.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mangezmieux-backend/liquibase/changelogs/changesets/insert-resource.xml b/mangezmieux-backend/liquibase/changelogs/changesets/insert-resource.xml new file mode 100644 index 0000000..63f7b1f --- /dev/null +++ b/mangezmieux-backend/liquibase/changelogs/changesets/insert-resource.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mangezmieux-backend/liquibase/changelogs/liquibase.properties b/mangezmieux-backend/liquibase/changelogs/liquibase.properties new file mode 100644 index 0000000..9dea210 --- /dev/null +++ b/mangezmieux-backend/liquibase/changelogs/liquibase.properties @@ -0,0 +1,4 @@ +url: jdbc:postgresql://127.0.0.1:5432/mangezmieux +username: postgres +password: mysecretpassword +logLevel: info \ No newline at end of file diff --git a/mangezmieux-backend/liquibase/changelogs/prepare-database.sql b/mangezmieux-backend/liquibase/changelogs/prepare-database.sql new file mode 100755 index 0000000..163894c --- /dev/null +++ b/mangezmieux-backend/liquibase/changelogs/prepare-database.sql @@ -0,0 +1,2 @@ +CREATE SCHEMA IF NOT EXISTS mangezmieux; +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/mangezmieux-backend/main.go b/mangezmieux-backend/main.go new file mode 100644 index 0000000..e52d65a --- /dev/null +++ b/mangezmieux-backend/main.go @@ -0,0 +1,7 @@ +package main + +import "mangezmieux-backend/cmd" + +func main() { + cmd.Execute() +}