mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'dev/hafizh' into 'development'
initial commit See merge request mbugroup/lti-api!1
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
# .air.toml
|
||||||
|
root = "."
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
cmd = "go build -o ./tmp/main ./cmd/api"
|
||||||
|
bin = "tmp/main"
|
||||||
|
full_bin = "APP_ENV=dev ./tmp/main"
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
exclude_dir = ["vendor", "tmp"]
|
||||||
|
|
||||||
|
[log]
|
||||||
|
time = true
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Git & CI
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.github
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
/tmp
|
||||||
|
/bin
|
||||||
|
/out
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Go specific
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Dependencies cache (biar tidak kebawa)
|
||||||
|
vendor/
|
||||||
|
go-build-cache/
|
||||||
|
go-mod-cache/
|
||||||
|
|
||||||
|
# Editor/IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# Env & secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!/.env.example
|
||||||
|
|
||||||
|
# Docker sendiri
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*.yml
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# server configuration
|
||||||
|
# Env value : prod || dev
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_HOST=0.0.0.0
|
||||||
|
APP_PORT=8080
|
||||||
|
APP_URL=http://localhost:8080
|
||||||
|
|
||||||
|
# database configuration
|
||||||
|
DB_HOST=postgresdb
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=changeme
|
||||||
|
DB_NAME=db_lti_erp
|
||||||
|
DB_PORT=5432
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
# JWT secret key
|
||||||
|
JWT_SECRET=changeme
|
||||||
|
# Number of minutes after which an access token expires
|
||||||
|
JWT_ACCESS_EXP_MINUTES=30
|
||||||
|
# Number of days after which a refresh token expires
|
||||||
|
JWT_REFRESH_EXP_DAYS=30
|
||||||
|
# Number of minutes after which a reset password token expires
|
||||||
|
JWT_RESET_PASSWORD_EXP_MINUTES=10
|
||||||
|
# Number of minutes after which a verify email token expires
|
||||||
|
JWT_VERIFY_EMAIL_EXP_MINUTES=10
|
||||||
|
|
||||||
|
# SMTP configuration options for the email service
|
||||||
|
SMTP_HOST=email-server
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USERNAME=email-server-username
|
||||||
|
SMTP_PASSWORD=changeme
|
||||||
|
EMAIL_FROM=support@yourapp.com
|
||||||
|
|
||||||
|
# OAuth2 configuration
|
||||||
|
GOOGLE_CLIENT_ID=yourapps.googleusercontent.com
|
||||||
|
GOOGLE_CLIENT_SECRET=changeme
|
||||||
|
REDIRECT_URL=http://localhost:3000/v1/auth/google-callback
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Air temp dir
|
||||||
|
tmp/
|
||||||
|
|
||||||
|
# Binaries
|
||||||
|
main
|
||||||
|
bin/
|
||||||
|
*.exe
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Logs & reports
|
||||||
|
*.log
|
||||||
|
*.txt
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# IDE / editor files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
+346
@@ -0,0 +1,346 @@
|
|||||||
|
# ## Config for golangci-lint v1.60.1
|
||||||
|
|
||||||
|
# run:
|
||||||
|
# # Timeout for analysis, e.g. 30s, 5m.
|
||||||
|
# # Default: 1m
|
||||||
|
# timeout: 3m
|
||||||
|
|
||||||
|
# # This file contains only configs which differ from defaults.
|
||||||
|
# # All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
|
||||||
|
# linters-settings:
|
||||||
|
# cyclop:
|
||||||
|
# # The maximal code complexity to report.
|
||||||
|
# # Default: 10
|
||||||
|
# max-complexity: 30
|
||||||
|
# # The maximal average package complexity.
|
||||||
|
# # If it's higher than 0.0 (float) the check is enabled
|
||||||
|
# # Default: 0.0
|
||||||
|
# package-average: 10.0
|
||||||
|
|
||||||
|
# errcheck:
|
||||||
|
# # Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
|
||||||
|
# # Such cases aren't reported by default.
|
||||||
|
# # Default: false
|
||||||
|
# check-type-assertions: true
|
||||||
|
|
||||||
|
# exhaustive:
|
||||||
|
# # Program elements to check for exhaustiveness.
|
||||||
|
# # Default: [ switch ]
|
||||||
|
# check:
|
||||||
|
# - switch
|
||||||
|
# - map
|
||||||
|
|
||||||
|
# exhaustruct:
|
||||||
|
# # List of regular expressions to exclude struct packages and their names from checks.
|
||||||
|
# # Regular expressions must match complete canonical struct package/name/structname.
|
||||||
|
# # Default: []
|
||||||
|
# exclude:
|
||||||
|
# # std libs
|
||||||
|
# - "^net/http.Client$"
|
||||||
|
# - "^net/http.Cookie$"
|
||||||
|
# - "^net/http.Request$"
|
||||||
|
# - "^net/http.Response$"
|
||||||
|
# - "^net/http.Server$"
|
||||||
|
# - "^net/http.Transport$"
|
||||||
|
# - "^net/url.URL$"
|
||||||
|
# - "^os/exec.Cmd$"
|
||||||
|
# - "^reflect.StructField$"
|
||||||
|
# # public libs
|
||||||
|
# - "^github.com/Shopify/sarama.Config$"
|
||||||
|
# - "^github.com/Shopify/sarama.ProducerMessage$"
|
||||||
|
# - "^github.com/mitchellh/mapstructure.DecoderConfig$"
|
||||||
|
# - "^github.com/prometheus/client_golang/.+Opts$"
|
||||||
|
# - "^github.com/spf13/cobra.Command$"
|
||||||
|
# - "^github.com/spf13/cobra.CompletionOptions$"
|
||||||
|
# - "^github.com/stretchr/testify/mock.Mock$"
|
||||||
|
# - "^github.com/testcontainers/testcontainers-go.+Request$"
|
||||||
|
# - "^github.com/testcontainers/testcontainers-go.FromDockerfile$"
|
||||||
|
# - "^golang.org/x/tools/go/analysis.Analyzer$"
|
||||||
|
# - "^google.golang.org/protobuf/.+Options$"
|
||||||
|
# - "^gopkg.in/yaml.v3.Node$"
|
||||||
|
|
||||||
|
# funlen:
|
||||||
|
# # Checks the number of lines in a function.
|
||||||
|
# # If lower than 0, disable the check.
|
||||||
|
# # Default: 60
|
||||||
|
# lines: 100
|
||||||
|
# # Checks the number of statements in a function.
|
||||||
|
# # If lower than 0, disable the check.
|
||||||
|
# # Default: 40
|
||||||
|
# statements: 50
|
||||||
|
# # Ignore comments when counting lines.
|
||||||
|
# # Default false
|
||||||
|
# ignore-comments: true
|
||||||
|
|
||||||
|
# gocognit:
|
||||||
|
# # Minimal code complexity to report.
|
||||||
|
# # Default: 30 (but we recommend 10-20)
|
||||||
|
# min-complexity: 20
|
||||||
|
|
||||||
|
# gocritic:
|
||||||
|
# # Settings passed to gocritic.
|
||||||
|
# # The settings key is the name of a supported gocritic checker.
|
||||||
|
# # The list of supported checkers can be find in https://go-critic.github.io/overview.
|
||||||
|
# settings:
|
||||||
|
# captLocal:
|
||||||
|
# # Whether to restrict checker to params only.
|
||||||
|
# # Default: true
|
||||||
|
# paramsOnly: false
|
||||||
|
# underef:
|
||||||
|
# # Whether to skip (*x).method() calls where x is a pointer receiver.
|
||||||
|
# # Default: true
|
||||||
|
# skipRecvDeref: false
|
||||||
|
|
||||||
|
# gomodguard:
|
||||||
|
# blocked:
|
||||||
|
# # List of blocked modules.
|
||||||
|
# # Default: []
|
||||||
|
# modules:
|
||||||
|
# - github.com/golang/protobuf:
|
||||||
|
# recommendations:
|
||||||
|
# - google.golang.org/protobuf
|
||||||
|
# reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules"
|
||||||
|
# - github.com/satori/go.uuid:
|
||||||
|
# recommendations:
|
||||||
|
# - github.com/google/uuid
|
||||||
|
# reason: "satori's package is not maintained"
|
||||||
|
# - github.com/gofrs/uuid:
|
||||||
|
# recommendations:
|
||||||
|
# - github.com/gofrs/uuid/v5
|
||||||
|
# reason: "gofrs' package was not go module before v5"
|
||||||
|
|
||||||
|
# govet:
|
||||||
|
# # Enable all analyzers.
|
||||||
|
# # Default: false
|
||||||
|
# enable-all: true
|
||||||
|
# # Disable analyzers by name.
|
||||||
|
# # Run `go tool vet help` to see all analyzers.
|
||||||
|
# # Default: []
|
||||||
|
# disable:
|
||||||
|
# - fieldalignment # too strict
|
||||||
|
# # Settings per analyzer.
|
||||||
|
# settings:
|
||||||
|
# shadow:
|
||||||
|
# # Whether to be strict about shadowing; can be noisy.
|
||||||
|
# # Default: false
|
||||||
|
# strict: true
|
||||||
|
|
||||||
|
# inamedparam:
|
||||||
|
# # Skips check for interface methods with only a single parameter.
|
||||||
|
# # Default: false
|
||||||
|
# skip-single-param: true
|
||||||
|
|
||||||
|
# mnd:
|
||||||
|
# # List of function patterns to exclude from analysis.
|
||||||
|
# # Values always ignored: `time.Date`,
|
||||||
|
# # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,
|
||||||
|
# # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.
|
||||||
|
# # Default: []
|
||||||
|
# ignored-functions:
|
||||||
|
# - args.Error
|
||||||
|
# - flag.Arg
|
||||||
|
# - flag.Duration.*
|
||||||
|
# - flag.Float.*
|
||||||
|
# - flag.Int.*
|
||||||
|
# - flag.Uint.*
|
||||||
|
# - os.Chmod
|
||||||
|
# - os.Mkdir.*
|
||||||
|
# - os.OpenFile
|
||||||
|
# - os.WriteFile
|
||||||
|
# - prometheus.ExponentialBuckets.*
|
||||||
|
# - prometheus.LinearBuckets
|
||||||
|
|
||||||
|
# nakedret:
|
||||||
|
# # Make an issue if func has more lines of code than this setting, and it has naked returns.
|
||||||
|
# # Default: 30
|
||||||
|
# max-func-lines: 0
|
||||||
|
|
||||||
|
# nolintlint:
|
||||||
|
# # Exclude following linters from requiring an explanation.
|
||||||
|
# # Default: []
|
||||||
|
# allow-no-explanation: [funlen, gocognit, lll]
|
||||||
|
# # Enable to require an explanation of nonzero length after each nolint directive.
|
||||||
|
# # Default: false
|
||||||
|
# require-explanation: true
|
||||||
|
# # Enable to require nolint directives to mention the specific linter being suppressed.
|
||||||
|
# # Default: false
|
||||||
|
# require-specific: true
|
||||||
|
|
||||||
|
# perfsprint:
|
||||||
|
# # Optimizes into strings concatenation.
|
||||||
|
# # Default: true
|
||||||
|
# strconcat: false
|
||||||
|
|
||||||
|
# rowserrcheck:
|
||||||
|
# # database/sql is always checked
|
||||||
|
# # Default: []
|
||||||
|
# packages:
|
||||||
|
# - github.com/jmoiron/sqlx
|
||||||
|
|
||||||
|
# sloglint:
|
||||||
|
# # Enforce not using global loggers.
|
||||||
|
# # Values:
|
||||||
|
# # - "": disabled
|
||||||
|
# # - "all": report all global loggers
|
||||||
|
# # - "default": report only the default slog logger
|
||||||
|
# # https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global
|
||||||
|
# # Default: ""
|
||||||
|
# no-global: "all"
|
||||||
|
# # Enforce using methods that accept a context.
|
||||||
|
# # Values:
|
||||||
|
# # - "": disabled
|
||||||
|
# # - "all": report all contextless calls
|
||||||
|
# # - "scope": report only if a context exists in the scope of the outermost function
|
||||||
|
# # https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only
|
||||||
|
# # Default: ""
|
||||||
|
# context: "scope"
|
||||||
|
|
||||||
|
# tenv:
|
||||||
|
# # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.
|
||||||
|
# # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.
|
||||||
|
# # Default: false
|
||||||
|
# all: true
|
||||||
|
|
||||||
|
# linters:
|
||||||
|
# disable-all: true
|
||||||
|
# enable:
|
||||||
|
# ## enabled by default
|
||||||
|
# - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases
|
||||||
|
# - gosimple # specializes in simplifying a code
|
||||||
|
# - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
|
||||||
|
# - ineffassign # detects when assignments to existing variables are not used
|
||||||
|
# - staticcheck # is a go vet on steroids, applying a ton of static analysis checks
|
||||||
|
# - typecheck # like the front-end of a Go compiler, parses and type-checks Go code
|
||||||
|
# - unused # checks for unused constants, variables, functions and types
|
||||||
|
# ## disabled by default
|
||||||
|
# - asasalint # checks for pass []any as any in variadic func(...any)
|
||||||
|
# - asciicheck # checks that your code does not contain non-ASCII identifiers
|
||||||
|
# - bidichk # checks for dangerous unicode character sequences
|
||||||
|
# - bodyclose # checks whether HTTP response body is closed successfully
|
||||||
|
# - canonicalheader # checks whether net/http.Header uses canonical header
|
||||||
|
# - cyclop # checks function and package cyclomatic complexity
|
||||||
|
# - dupl # tool for code clone detection
|
||||||
|
# - durationcheck # checks for two durations multiplied together
|
||||||
|
# - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
|
||||||
|
# - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13
|
||||||
|
# - exhaustive # checks exhaustiveness of enum switch statements
|
||||||
|
# - fatcontext # detects nested contexts in loops
|
||||||
|
# - forbidigo # forbids identifiers
|
||||||
|
# - funlen # tool for detection of long functions
|
||||||
|
# - gocheckcompilerdirectives # validates go compiler directive comments (//go:)
|
||||||
|
# #- gochecknoglobals # checks that no global variables exist
|
||||||
|
# #- gochecknoinits # checks that no init functions are present in Go code
|
||||||
|
# - gochecksumtype # checks exhaustiveness on Go "sum types"
|
||||||
|
# - gocognit # computes and checks the cognitive complexity of functions
|
||||||
|
# - goconst # finds repeated strings that could be replaced by a constant
|
||||||
|
# - gocritic # provides diagnostics that check for bugs, performance and style issues
|
||||||
|
# - gocyclo # computes and checks the cyclomatic complexity of functions
|
||||||
|
# #- godot # checks if comments end in a period
|
||||||
|
# - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt
|
||||||
|
# - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
|
||||||
|
# - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations
|
||||||
|
# - goprintffuncname # checks that printf-like functions are named with f at the end
|
||||||
|
# - gosec # inspects source code for security problems
|
||||||
|
# - intrange # finds places where for loops could make use of an integer range
|
||||||
|
# - lll # reports long lines
|
||||||
|
# - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
|
||||||
|
# - makezero # finds slice declarations with non-zero initial length
|
||||||
|
# - mirror # reports wrong mirror patterns of bytes/strings usage
|
||||||
|
# #- mnd # detects magic numbers
|
||||||
|
# - musttag # enforces field tags in (un)marshaled structs
|
||||||
|
# - nakedret # finds naked returns in functions greater than a specified function length
|
||||||
|
# - nestif # reports deeply nested if statements
|
||||||
|
# - nilerr # finds the code that returns nil even if it checks that the error is not nil
|
||||||
|
# - nilnil # checks that there is no simultaneous return of nil error and an invalid value
|
||||||
|
# - noctx # finds sending http request without context.Context
|
||||||
|
# - nolintlint # reports ill-formed or insufficient nolint directives
|
||||||
|
# - nonamedreturns # reports all named returns
|
||||||
|
# - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL
|
||||||
|
# - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative
|
||||||
|
# - predeclared # finds code that shadows one of Go's predeclared identifiers
|
||||||
|
# - promlinter # checks Prometheus metrics naming via promlint
|
||||||
|
# - protogetter # reports direct reads from proto message fields when getters should be used
|
||||||
|
# - reassign # checks that package variables are not reassigned
|
||||||
|
# - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint
|
||||||
|
# - rowserrcheck # checks whether Err of rows is checked successfully
|
||||||
|
# - sloglint # ensure consistent code style when using log/slog
|
||||||
|
# - spancheck # checks for mistakes with OpenTelemetry/Census spans
|
||||||
|
# - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
|
||||||
|
# - stylecheck # is a replacement for golint
|
||||||
|
# - tenv # detects using os.Setenv instead of t.Setenv since Go1.17
|
||||||
|
# - testableexamples # checks if examples are testable (have an expected output)
|
||||||
|
# #- testifylint # checks usage of github.com/stretchr/testify
|
||||||
|
# - testpackage # makes you use a separate _test package
|
||||||
|
# - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
|
||||||
|
# - unconvert # removes unnecessary type conversions
|
||||||
|
# - unparam # reports unused function parameters
|
||||||
|
# - usestdlibvars # detects the possibility to use variables/constants from the Go standard library
|
||||||
|
# - wastedassign # finds wasted assignment statements
|
||||||
|
# - whitespace # detects leading and trailing whitespace
|
||||||
|
|
||||||
|
# ## you may want to enable
|
||||||
|
# #- decorder # checks declaration order and count of types, constants, variables and functions
|
||||||
|
# #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized
|
||||||
|
# #- gci # controls golang package import order and makes it always deterministic
|
||||||
|
# #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega
|
||||||
|
# #- godox # detects FIXME, TODO and other comment keywords
|
||||||
|
# #- goheader # checks is file header matches to pattern
|
||||||
|
# #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters
|
||||||
|
# #- interfacebloat # checks the number of methods inside an interface
|
||||||
|
# #- ireturn # accept interfaces, return concrete types
|
||||||
|
# #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated
|
||||||
|
# #- tagalign # checks that struct tags are well aligned
|
||||||
|
# #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope
|
||||||
|
# #- wrapcheck # checks that errors returned from external packages are wrapped
|
||||||
|
# #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event
|
||||||
|
|
||||||
|
# ## disabled
|
||||||
|
# #- containedctx # detects struct contained context.Context field
|
||||||
|
# #- contextcheck # [too many false positives] checks the function whether use a non-inherited context
|
||||||
|
# #- copyloopvar # [not necessary from Go 1.22] detects places where loop variables are copied
|
||||||
|
# #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages
|
||||||
|
# #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
|
||||||
|
# #- dupword # [useless without config] checks for duplicate words in the source code
|
||||||
|
# #- err113 # [too strict] checks the errors handling expressions
|
||||||
|
# #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted
|
||||||
|
# #- execinquery # [deprecated] checks query string in Query function which reads your Go src files and warning it finds
|
||||||
|
# #- exportloopref # [not necessary from Go 1.22] checks for pointers to enclosing loop variables
|
||||||
|
# #- forcetypeassert # [replaced by errcheck] finds forced type assertions
|
||||||
|
# #- gofmt # [replaced by goimports] checks whether code was gofmt-ed
|
||||||
|
# #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed
|
||||||
|
# #- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase
|
||||||
|
# #- grouper # analyzes expression groups
|
||||||
|
# #- importas # enforces consistent import aliases
|
||||||
|
# #- maintidx # measures the maintainability index of each function
|
||||||
|
# #- misspell # [useless] finds commonly misspelled English words in comments
|
||||||
|
# #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity
|
||||||
|
# #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test
|
||||||
|
# #- tagliatelle # checks the struct tags
|
||||||
|
# #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers
|
||||||
|
# #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines
|
||||||
|
|
||||||
|
# issues:
|
||||||
|
# # Maximum count of issues with the same text.
|
||||||
|
# # Set to 0 to disable.
|
||||||
|
# # Default: 3
|
||||||
|
# max-same-issues: 50
|
||||||
|
|
||||||
|
# exclude-rules:
|
||||||
|
# - source: "(noinspection|TODO)"
|
||||||
|
# linters: [godot]
|
||||||
|
# - source: "//noinspection"
|
||||||
|
# linters: [gocritic]
|
||||||
|
# - path: "example\\.go"
|
||||||
|
# linters:
|
||||||
|
# - lll
|
||||||
|
# - path: "_test\\.go"
|
||||||
|
# linters:
|
||||||
|
# - bodyclose
|
||||||
|
# - dupl
|
||||||
|
# - funlen
|
||||||
|
# - goconst
|
||||||
|
# - gosec
|
||||||
|
# - noctx
|
||||||
|
# - wrapcheck
|
||||||
|
# - lll
|
||||||
|
# - testpackage
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
FROM golang:1.23-alpine
|
||||||
|
|
||||||
|
# Install dependensi dasar
|
||||||
|
RUN apk add --no-cache git curl bash build-base
|
||||||
|
|
||||||
|
# Install Air (pakai repo baru air-verse)
|
||||||
|
RUN go install github.com/air-verse/air@v1.52.3
|
||||||
|
|
||||||
|
WORKDIR /golang-boilerplate
|
||||||
|
|
||||||
|
# Cache dependencies
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["air", "-c", ".air.toml"]
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
FROM golang:1.23-alpine AS builder
|
||||||
|
|
||||||
|
# Install tools build
|
||||||
|
RUN apk add --no-cache git curl bash build-base
|
||||||
|
|
||||||
|
WORKDIR /golang-boilerplate
|
||||||
|
|
||||||
|
# Cache dependencies
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build binary dari entrypoint (cmd/api/main.go)
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -o app ./cmd/api
|
||||||
|
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
# Install tools
|
||||||
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
|
WORKDIR /golang-boilerplate
|
||||||
|
|
||||||
|
# Copy binary hasil build
|
||||||
|
COPY --from=builder /golang-boilerplate/app .
|
||||||
|
|
||||||
|
# Copy file env example
|
||||||
|
COPY --from=builder /golang-boilerplate/.env.example .env
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["./app"]
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# --- Load .env kalau ada, dan export ke shell child ---
|
||||||
|
ifneq (,$(wildcard .env))
|
||||||
|
include .env
|
||||||
|
export
|
||||||
|
endif
|
||||||
|
|
||||||
|
# --- Konfigurasi umum ---
|
||||||
|
COMPOSE ?= docker compose -f docker-compose.dev.yml
|
||||||
|
NETWORK ?= boilerplate-template_go-network
|
||||||
|
MIGRATE_IMAGE ?= migrate/migrate
|
||||||
|
MIGRATIONS_DIR := $(PWD)/internal/database/migrations
|
||||||
|
|
||||||
|
# Fallback agar tetap jalan meski .env kosong
|
||||||
|
DB_HOST ?= postgresdb
|
||||||
|
DB_PORT ?= 5432
|
||||||
|
DB_USER ?= postgres
|
||||||
|
DB_PASSWORD ?= postgres
|
||||||
|
DB_NAME ?= db_lti_erp
|
||||||
|
|
||||||
|
DB_URL := postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
|
||||||
|
|
||||||
|
# Tunggu DB ready memakai pg_isready dari image postgres
|
||||||
|
WAIT_DB := docker run --rm --network $(NETWORK) postgres:alpine \
|
||||||
|
sh -c 'until pg_isready -h $(DB_HOST) -p $(DB_PORT) -U $(DB_USER) -d $(DB_NAME); do echo "waiting for postgres..."; sleep 1; done'
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
.DEFAULT_GOAL := start
|
||||||
|
|
||||||
|
# --- Daftar phony targets ---
|
||||||
|
.PHONY: start build lint gen \
|
||||||
|
db-up wait-db \
|
||||||
|
migration-% migrate-up migrate-down migrate-fresh \
|
||||||
|
seed \
|
||||||
|
docker-dev docker-prod docker-down docker-nuke docker-cache psql
|
||||||
|
|
||||||
|
# --- Go workflow ---
|
||||||
|
start:
|
||||||
|
@go run cmd/api/main.go
|
||||||
|
|
||||||
|
build:
|
||||||
|
@go build -o tmp/app ./cmd/api
|
||||||
|
|
||||||
|
lint:
|
||||||
|
@golangci-lint run
|
||||||
|
|
||||||
|
# --- Compose / DB helpers ---
|
||||||
|
db-up:
|
||||||
|
@$(COMPOSE) up -d postgresdb
|
||||||
|
|
||||||
|
wait-db:
|
||||||
|
@$(WAIT_DB)
|
||||||
|
|
||||||
|
# --- Migration (pembuatan file) ---
|
||||||
|
# Contoh: make migration-create_users_table
|
||||||
|
# ":" akan diubah ke "_" (biar aman untuk nama file)
|
||||||
|
migration-%:
|
||||||
|
@migrate create -ext sql -dir internal/database/migrations $(subst :,_,$*)
|
||||||
|
|
||||||
|
# --- Migration (apply via docker image 'migrate') ---
|
||||||
|
migrate-up: db-up wait-db
|
||||||
|
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
||||||
|
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" up
|
||||||
|
|
||||||
|
# Contoh:
|
||||||
|
# make migrate-down step=2 → rollback 2 step
|
||||||
|
# make migrate-down → rollback semua
|
||||||
|
|
||||||
|
migrate-down: db-up wait-db
|
||||||
|
@if [ -n "$(step)" ]; then \
|
||||||
|
echo "⬇️ Migrating down $(step) step(s)..."; \
|
||||||
|
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
||||||
|
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down $(step); \
|
||||||
|
else \
|
||||||
|
echo "⬇️ Migrating down ALL steps..."; \
|
||||||
|
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
||||||
|
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down -all; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
migrate-fresh: migrate-down migrate-up
|
||||||
|
@true
|
||||||
|
|
||||||
|
# Pakai: make migrate-force v=20250917120000
|
||||||
|
migrate-force:
|
||||||
|
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
||||||
|
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" force $(v)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Seeder ---
|
||||||
|
seed: db-up wait-db
|
||||||
|
@$(COMPOSE) run --rm app go run cmd/seed/main.go
|
||||||
|
|
||||||
|
# --- Docker orchestration convenience ---
|
||||||
|
docker-dev:
|
||||||
|
@$(COMPOSE) up --build -d
|
||||||
|
|
||||||
|
docker-prod:
|
||||||
|
@docker compose -f docker-compose.prod.yml up --build -d
|
||||||
|
|
||||||
|
docker-down:
|
||||||
|
@$(COMPOSE) down --remove-orphans
|
||||||
|
|
||||||
|
# ⚠️ Akan menghapus container, images dan volumes.
|
||||||
|
docker-nuke:
|
||||||
|
@$(COMPOSE) down --rmi all --volumes --remove-orphans
|
||||||
|
|
||||||
|
docker-cache:
|
||||||
|
@docker builder prune -f
|
||||||
|
|
||||||
|
# --- PSQL shell ke DB di container ---
|
||||||
|
psql: db-up
|
||||||
|
@$(COMPOSE) exec -it postgresdb psql -U $(DB_USER) -d $(DB_NAME)
|
||||||
|
|
||||||
|
# Single feature
|
||||||
|
# example: make gen feat=product-category
|
||||||
|
|
||||||
|
# Sub feature
|
||||||
|
# make gen feat=master/area
|
||||||
|
gen:
|
||||||
|
@go run tools/gen.go $(feat)
|
||||||
|
# @goimports -w internal
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Golang Boilerplate (golang-boilerplate)
|
||||||
|
|
||||||
|
Boilerplate RESTful API, built with **Go, Fiber, GORM** and **PostgreSQL**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Tech Stack
|
||||||
|
|
||||||
|
- **Go + Fiber** — Web framework
|
||||||
|
- **GORM** — ORM for PostgreSQL
|
||||||
|
- **PostgreSQL** — Relational database
|
||||||
|
- **go-playground/validator** — Input validation
|
||||||
|
- **JWT** — Authentication
|
||||||
|
- **Logrus** — Logging
|
||||||
|
- **Fiber middleware** — Rate limiting, CORS, recovery, logger
|
||||||
|
- **Air** — Hot reload for development
|
||||||
|
- **Docker + Docker Compose** — Containerization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
### 1. Clone Project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/hafizhproject45/Golang-Boilerplate.git
|
||||||
|
cd golang-boilerplate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure Environment
|
||||||
|
|
||||||
|
Copy .env.example to .env and adjust the variables (e.g. DATABASE_URL, JWT secrets, etc).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Setup Docker
|
||||||
|
|
||||||
|
Run initial docker.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make docker-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Migrate Database
|
||||||
|
|
||||||
|
Run initial migrations and generate views.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make migrate-up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Run App
|
||||||
|
|
||||||
|
Run project via Docker
|
||||||
|
|
||||||
|
### 6. Create New Module
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make gen feat=user
|
||||||
|
```
|
||||||
|
|
||||||
|
output:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmd/
|
||||||
|
├── api/
|
||||||
|
│ └── main.go # Application entrypoint (initialize Fiber, load config, connect DB, register route)
|
||||||
|
|
||||||
|
internal/
|
||||||
|
├── config/ # App config (env loader, logger, app settings)
|
||||||
|
│
|
||||||
|
├── database/ # Database connection + migration setup
|
||||||
|
│
|
||||||
|
├── middleware/ # Global Fiber middleware (auth, logger, recovery, rate limiting)
|
||||||
|
│
|
||||||
|
├── modules/ # Feature modules (users, products, suppliers, etc.)
|
||||||
|
│ ├── <module>/
|
||||||
|
│ │ ├── controllers/ # HTTP handler layer (receive request, call service, return response)
|
||||||
|
│ │ ├── dto/ # Data Transfer Objects (request & response payloads, separate from models)
|
||||||
|
│ │ ├── models/ # GORM models (represent database tables/entities)
|
||||||
|
│ │ ├── repositories/ # Data access layer (queries to DB, CRUD abstraction)
|
||||||
|
│ │ ├── services/ # Business logic layer (process rules, orchestrate repository calls)
|
||||||
|
│ │ ├── validation/ # Request validation (custom rules per module)
|
||||||
|
│ │ ├── module.go # Module bootstrapper (wire controller, service, repository together)
|
||||||
|
│ │ └── route.go # Module route (register module routes into Fiber app)
|
||||||
|
│
|
||||||
|
├── repository/ # Shared repositories (reusable DB access layer across multiple modules)
|
||||||
|
│
|
||||||
|
├── response/ # Standardized API responses (success, error, pagination)
|
||||||
|
│
|
||||||
|
├── utils/ # Helper functions (JWT, hashing, constants, enums, etc.)
|
||||||
|
│
|
||||||
|
├── validation/ # Shared request validation structs & rules
|
||||||
|
│
|
||||||
|
└── route/ # Central route aggregator (load all module routes into main app)
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✨ Author
|
||||||
|
|
||||||
|
Hafizh Athallah Yovanka
|
||||||
|
|
||||||
|
## 📃 License
|
||||||
|
|
||||||
|
Free to use
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/config"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/database"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/middleware"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/route"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/helmet"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
app := setupFiberApp()
|
||||||
|
db := setupDatabase()
|
||||||
|
defer closeDatabase(db)
|
||||||
|
rdb := setupRedis()
|
||||||
|
defer rdb.Close()
|
||||||
|
setupRoutes(app, db, rdb)
|
||||||
|
|
||||||
|
address := fmt.Sprintf("%s:%d", config.AppHost, config.AppPort)
|
||||||
|
|
||||||
|
// Start server and handle graceful shutdown
|
||||||
|
serverErrors := make(chan error, 1)
|
||||||
|
go startServer(app, address, serverErrors)
|
||||||
|
handleGracefulShutdown(ctx, app, serverErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRedis() *redis.Client {
|
||||||
|
redisURL := os.Getenv("REDIS_URL")
|
||||||
|
if redisURL == "" {
|
||||||
|
redisURL = "redis://redis:6379/0"
|
||||||
|
}
|
||||||
|
opt, err := redis.ParseURL(redisURL)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Fatalf("Redis URL parse error: %v", err)
|
||||||
|
}
|
||||||
|
rdb := redis.NewClient(opt)
|
||||||
|
if err := rdb.Ping(context.Background()).Err(); err != nil {
|
||||||
|
utils.Log.Fatalf("Redis ping failed: %v", err)
|
||||||
|
}
|
||||||
|
utils.Log.Infof("Redis connected: %s", redisURL)
|
||||||
|
return rdb
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupFiberApp() *fiber.App {
|
||||||
|
app := fiber.New(config.FiberConfig())
|
||||||
|
|
||||||
|
// Middleware setup
|
||||||
|
app.Use("/api/auth", middleware.LimiterConfig())
|
||||||
|
app.Use(middleware.LoggerConfig())
|
||||||
|
app.Use(helmet.New())
|
||||||
|
app.Use(compress.New())
|
||||||
|
app.Use(cors.New())
|
||||||
|
app.Use(middleware.RecoverConfig())
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDatabase() *gorm.DB {
|
||||||
|
db := database.Connect(config.DBHost, config.DBName)
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRoutes(app *fiber.App, db *gorm.DB, rdb *redis.Client) {
|
||||||
|
|
||||||
|
// route.Routes(app, db)
|
||||||
|
// app.Use(utils.NotFoundHandler)
|
||||||
|
app.Get("/healthz", func(c *fiber.Ctx) error {
|
||||||
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||||
|
"status": "ok",
|
||||||
|
"service": "api",
|
||||||
|
"version": os.Getenv("VERSION"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/readyz", func(c *fiber.Ctx) error {
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"status": "error", "db": "unavailable", "redis": "unknown",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(c.Context(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dbOK := sqlDB.PingContext(ctx) == nil
|
||||||
|
redisOK := rdb.Ping(ctx).Err() == nil
|
||||||
|
|
||||||
|
status := fiber.StatusOK
|
||||||
|
statusText := "ok"
|
||||||
|
if !dbOK || !redisOK {
|
||||||
|
status = fiber.StatusServiceUnavailable
|
||||||
|
statusText = "degraded"
|
||||||
|
}
|
||||||
|
body := fiber.Map{
|
||||||
|
"status": statusText,
|
||||||
|
"db": map[bool]string{true: "up", false: "down"}[dbOK],
|
||||||
|
"redis": map[bool]string{true: "up", false: "down"}[redisOK],
|
||||||
|
}
|
||||||
|
return c.Status(status).JSON(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
route.Routes(app, db)
|
||||||
|
app.Use(utils.NotFoundHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startServer(app *fiber.App, address string, errs chan<- error) {
|
||||||
|
if err := app.Listen(address); err != nil {
|
||||||
|
errs <- fmt.Errorf("error starting server: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeDatabase(db *gorm.DB) {
|
||||||
|
sqlDB, errDB := db.DB()
|
||||||
|
if errDB != nil {
|
||||||
|
utils.Log.Errorf("Error getting database instance: %v", errDB)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sqlDB.Close(); err != nil {
|
||||||
|
utils.Log.Errorf("Error closing database connection: %v", err)
|
||||||
|
} else {
|
||||||
|
utils.Log.Info("Database connection closed successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGracefulShutdown(ctx context.Context, app *fiber.App, serverErrors <-chan error) {
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-serverErrors:
|
||||||
|
utils.Log.Fatalf("Server error: %v", err)
|
||||||
|
case <-quit:
|
||||||
|
utils.Log.Info("Shutting down server...")
|
||||||
|
if err := app.Shutdown(); err != nil {
|
||||||
|
utils.Log.Fatalf("Error during server shutdown: %v", err)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
utils.Log.Info("Server exiting due to context cancellation")
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.Log.Info("Server exited")
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/config"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/database"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/database/seed"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
db := database.Connect(config.DBHost, config.DBName)
|
||||||
|
|
||||||
|
if err := seed.Run(db); err != nil {
|
||||||
|
log.Fatalf("❌ Failed run seeder: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("✅ Seed Successfully")
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
services:
|
||||||
|
postgresdb:
|
||||||
|
image: postgres:alpine
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "${DB_PORT:-5432}:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${DB_USER:-postgres}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||||
|
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
|
||||||
|
volumes:
|
||||||
|
- dbdata:/var/lib/postgresql/data
|
||||||
|
- ./internal/database/init:/docker-entrypoint-initdb.d
|
||||||
|
networks: [go-network]
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
networks: [go-network]
|
||||||
|
|
||||||
|
mailhog:
|
||||||
|
image: mailhog/mailhog:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "1025:1025" # SMTP
|
||||||
|
- "8025:8025" # Web UI
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8025/ || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks: [go-network]
|
||||||
|
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
image: cosmtrek/air:v1.52.3
|
||||||
|
working_dir: /golang-boilerplate
|
||||||
|
volumes:
|
||||||
|
- .:/golang-boilerplate
|
||||||
|
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key:ro
|
||||||
|
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub:ro
|
||||||
|
command: air -c .air.toml
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
DB_HOST: postgresdb
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: ${DB_USER:-postgres}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||||
|
DB_NAME: ${DB_NAME:-db_lti_erp}
|
||||||
|
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
|
||||||
|
SMTP_HOST: ${SMTP_HOST:-mailhog}
|
||||||
|
SMTP_PORT: ${SMTP_PORT:-1025}
|
||||||
|
ports:
|
||||||
|
- "${APP_PORT:-8080}:8080"
|
||||||
|
depends_on:
|
||||||
|
postgresdb:
|
||||||
|
condition: service_healthy
|
||||||
|
networks: [go-network]
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dbdata:
|
||||||
|
go-mod-cache:
|
||||||
|
go-build-cache:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
go-network:
|
||||||
|
name: boilerplate-template_go-network
|
||||||
|
driver: bridge
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
services:
|
||||||
|
postgresdb:
|
||||||
|
image: postgres:alpine
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "${DB_PORT:-5432}:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${DB_USER:-postgres}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||||
|
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
|
||||||
|
volumes:
|
||||||
|
- dbdata:/var/lib/postgresql/data
|
||||||
|
- ./internal/database/init:/docker-entrypoint-initdb.d
|
||||||
|
networks: [go-network]
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.prod
|
||||||
|
image: golang-boilerplate-app
|
||||||
|
working_dir: /golang-boilerplate
|
||||||
|
command: ./app # asumsi Dockerfile.prod menghasilkan binary bernama "app"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
DB_HOST: postgresdb
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: ${DB_USER:-postgres}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||||
|
DB_NAME: ${DB_NAME:-db_lti_erp}
|
||||||
|
ports:
|
||||||
|
- "${APP_PORT:-8080}:8080"
|
||||||
|
depends_on:
|
||||||
|
postgresdb:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: on-failure
|
||||||
|
networks: [go-network]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dbdata:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
go-network:
|
||||||
|
name: boilerplate-template_go-network
|
||||||
|
driver: bridge
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
module github.com/hafizhproject45/Golang-Boilerplate.git
|
||||||
|
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.12.1
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0
|
||||||
|
github.com/gofiber/contrib/jwt v1.0.10
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.5
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
|
github.com/redis/go-redis/v9 v9.14.0
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/spf13/viper v1.19.0
|
||||||
|
golang.org/x/crypto v0.33.0
|
||||||
|
golang.org/x/oauth2 v0.22.0
|
||||||
|
gorm.io/driver/postgres v1.5.9
|
||||||
|
gorm.io/gorm v1.25.11
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||||
|
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/philhofer/fwd v1.1.2 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0 // 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/tinylib/msgp v1.1.8 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.55.0 // indirect
|
||||||
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
|
golang.org/x/net v0.35.0 // indirect
|
||||||
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||||
|
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||||
|
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
|
||||||
|
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
||||||
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
|
github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24=
|
||||||
|
github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
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/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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
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.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
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.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/gofiber/contrib/jwt v1.0.10 h1:/ilGepl6i0Bntl0Zcd+lAzagY8BiS1+fEiAj32HMApk=
|
||||||
|
github.com/gofiber/contrib/jwt v1.0.10/go.mod h1:1qBENE6sZ6PPT4xIpBzx1VxeyROQO7sj48OlM1I9qdU=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||||
|
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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
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/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
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/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/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||||
|
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||||
|
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/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
|
||||||
|
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
|
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/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.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/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||||
|
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||||
|
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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
|
||||||
|
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
|
||||||
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
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 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
|
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||||
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
||||||
|
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
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=
|
||||||
|
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
||||||
|
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||||
|
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||||
|
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthCfg struct {
|
||||||
|
Issuer string
|
||||||
|
JWTSecret string
|
||||||
|
AccessTTL time.Duration
|
||||||
|
RefreshTTL time.Duration
|
||||||
|
RefreshCookieName string
|
||||||
|
RefreshCookiePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadAuth() AuthCfg {
|
||||||
|
return AuthCfg{
|
||||||
|
JWTSecret: getenv("JWT_SECRET", "dev-secret-change-me"),
|
||||||
|
AccessTTL: getenvDuration("ACCESS_TTL", 10*time.Minute),
|
||||||
|
RefreshTTL: getenvDuration("REFRESH_TTL", 30*24*time.Hour),
|
||||||
|
RefreshCookieName: getenv("REFRESH_COOKIE_NAME", "rt"),
|
||||||
|
RefreshCookiePath: getenv("REFRESH_COOKIE_PATH", "/api/auth"),
|
||||||
|
Issuer: getenv("ISSUER", "http://localhost:8080"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getenv(k, def string) string {
|
||||||
|
if v := os.Getenv(k); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
func getenvDuration(k string, def time.Duration) time.Duration {
|
||||||
|
if v := os.Getenv(k); v != "" {
|
||||||
|
d, err := time.ParseDuration(v)
|
||||||
|
if err == nil {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
IsProd bool
|
||||||
|
AppHost string
|
||||||
|
Version string
|
||||||
|
LogLevel string
|
||||||
|
AppPort int
|
||||||
|
DBHost string
|
||||||
|
DBUser string
|
||||||
|
DBPassword string
|
||||||
|
DBName string
|
||||||
|
DBPort int
|
||||||
|
JWTSecret string
|
||||||
|
JWTAccessExp int
|
||||||
|
JWTRefreshExp int
|
||||||
|
JWTResetPasswordExp int
|
||||||
|
JWTVerifyEmailExp int
|
||||||
|
PostgresDSN string
|
||||||
|
RedisURL string
|
||||||
|
Issuer string
|
||||||
|
SMTPHost string
|
||||||
|
SMTPPort int
|
||||||
|
SMTPUsername string
|
||||||
|
SMTPPassword string
|
||||||
|
EmailFrom string
|
||||||
|
GoogleClientID string
|
||||||
|
GoogleClientSecret string
|
||||||
|
RedirectURL string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
loadConfig()
|
||||||
|
|
||||||
|
// server configuration
|
||||||
|
IsProd = viper.GetString("APP_ENV") == "prod"
|
||||||
|
// AppHost = viper.GetString("APP_HOST")
|
||||||
|
// AppPort = viper.GetInt("APP_PORT")
|
||||||
|
AppHost = viper.GetString("APP_HOST")
|
||||||
|
if AppHost == "" {
|
||||||
|
AppHost = "0.0.0.0"
|
||||||
|
}
|
||||||
|
AppPort = viper.GetInt("APP_PORT")
|
||||||
|
if AppPort == 0 {
|
||||||
|
AppPort = 8080
|
||||||
|
}
|
||||||
|
Version = viper.GetString("VERSION")
|
||||||
|
LogLevel = viper.GetString("LOG_LEVEL")
|
||||||
|
|
||||||
|
// database configuration
|
||||||
|
DBHost = viper.GetString("DB_HOST")
|
||||||
|
DBUser = viper.GetString("DB_USER")
|
||||||
|
DBPassword = viper.GetString("DB_PASSWORD")
|
||||||
|
DBName = viper.GetString("DB_NAME")
|
||||||
|
DBPort = viper.GetInt("DB_PORT")
|
||||||
|
PostgresDSN = viper.GetString("POSTGRES_DSN")
|
||||||
|
if PostgresDSN == "" {
|
||||||
|
PostgresDSN = fmt.Sprintf(
|
||||||
|
"postgres://%s:%s@%s:%d/%s?sslmode=disable",
|
||||||
|
DBUser, DBPassword, DBHost, DBPort, DBName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// jwt configuration
|
||||||
|
JWTSecret = viper.GetString("JWT_SECRET")
|
||||||
|
JWTAccessExp = viper.GetInt("JWT_ACCESS_EXP_MINUTES")
|
||||||
|
JWTRefreshExp = viper.GetInt("JWT_REFRESH_EXP_DAYS")
|
||||||
|
JWTResetPasswordExp = viper.GetInt("JWT_RESET_PASSWORD_EXP_MINUTES")
|
||||||
|
JWTVerifyEmailExp = viper.GetInt("JWT_VERIFY_EMAIL_EXP_MINUTES")
|
||||||
|
|
||||||
|
// Redis / OIDC
|
||||||
|
RedisURL = viper.GetString("REDIS_URL")
|
||||||
|
if RedisURL == "" {
|
||||||
|
RedisURL = "redis://redis:6379/0"
|
||||||
|
}
|
||||||
|
Issuer = viper.GetString("ISSUER")
|
||||||
|
if Issuer == "" {
|
||||||
|
// fallback ke SSO_ISSUER jika kamu sudah pakai itu sebelumnya
|
||||||
|
Issuer = viper.GetString("SSO_ISSUER")
|
||||||
|
}
|
||||||
|
// SMTP configuration
|
||||||
|
SMTPHost = viper.GetString("SMTP_HOST")
|
||||||
|
SMTPPort = viper.GetInt("SMTP_PORT")
|
||||||
|
SMTPUsername = viper.GetString("SMTP_USERNAME")
|
||||||
|
SMTPPassword = viper.GetString("SMTP_PASSWORD")
|
||||||
|
EmailFrom = viper.GetString("EMAIL_FROM")
|
||||||
|
|
||||||
|
// oauth2 configuration
|
||||||
|
GoogleClientID = viper.GetString("GOOGLE_CLIENT_ID")
|
||||||
|
GoogleClientSecret = viper.GetString("GOOGLE_CLIENT_SECRET")
|
||||||
|
RedirectURL = viper.GetString("REDIRECT_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig() {
|
||||||
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
|
viper.SetConfigFile(".env")
|
||||||
|
if err := viper.ReadInConfig(); err == nil {
|
||||||
|
utils.Log.Info("Config file loaded from .env")
|
||||||
|
} else {
|
||||||
|
utils.Log.Warn("No .env file found, using environment variables only")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FiberConfig() fiber.Config {
|
||||||
|
return fiber.Config{
|
||||||
|
Prefork: IsProd,
|
||||||
|
CaseSensitive: true,
|
||||||
|
ServerHeader: "Fiber",
|
||||||
|
AppName: "Fiber API",
|
||||||
|
ErrorHandler: utils.ErrorHandler,
|
||||||
|
JSONEncoder: sonic.Marshal,
|
||||||
|
JSONDecoder: sonic.Unmarshal,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/google"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
GoogleLoginConfig oauth2.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
var AppConfig Config
|
||||||
|
|
||||||
|
func GoogleConfig() oauth2.Config {
|
||||||
|
AppConfig.GoogleLoginConfig = oauth2.Config{
|
||||||
|
RedirectURL: RedirectURL,
|
||||||
|
ClientID: GoogleClientID,
|
||||||
|
ClientSecret: GoogleClientSecret,
|
||||||
|
Scopes: []string{
|
||||||
|
"https://www.googleapis.com/auth/userinfo.email",
|
||||||
|
"https://www.googleapis.com/auth/userinfo.profile",
|
||||||
|
},
|
||||||
|
Endpoint: google.Endpoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppConfig.GoogleLoginConfig
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
var allRoles = map[string][]string{
|
||||||
|
"user": {},
|
||||||
|
"admin": {"getUsers", "manageUsers"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var Roles = getKeys(allRoles)
|
||||||
|
var RoleRights = allRoles
|
||||||
|
|
||||||
|
func getKeys(m map[string][]string) []string {
|
||||||
|
keys := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
const (
|
||||||
|
TokenTypeAccess = "access"
|
||||||
|
TokenTypeRefresh = "refresh"
|
||||||
|
TokenTypeResetPassword = "resetPassword"
|
||||||
|
TokenTypeVerifyEmail = "verifyEmail"
|
||||||
|
)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/config"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/utils"
|
||||||
|
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Connect(dbHost, dbName string) *gorm.DB {
|
||||||
|
dsn := fmt.Sprintf(
|
||||||
|
"host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai",
|
||||||
|
dbHost, config.DBUser, config.DBPassword, dbName, config.DBPort,
|
||||||
|
)
|
||||||
|
|
||||||
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Info),
|
||||||
|
SkipDefaultTransaction: true,
|
||||||
|
PrepareStmt: true,
|
||||||
|
TranslateError: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Errorf("Failed to connect to database: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDB, errDB := db.DB()
|
||||||
|
if errDB != nil {
|
||||||
|
utils.Log.Errorf("Failed to connect to database: %+v", errDB)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config connection pooling
|
||||||
|
sqlDB.SetMaxIdleConns(10)
|
||||||
|
sqlDB.SetMaxOpenConns(100)
|
||||||
|
sqlDB.SetConnMaxLifetime(60 * time.Minute)
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
CREATE DATABASE IF NOT EXISTS db_lti_erp;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS users;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Users
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package seed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
mUser "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/models"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run(db *gorm.DB) error {
|
||||||
|
return db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
// pw, err := secure.Hash("asdasdasd", nil)
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ===== Users (user) =====
|
||||||
|
user := mUser.User{
|
||||||
|
Name: "Super Admin",
|
||||||
|
}
|
||||||
|
if err := tx.Where("email = ?", user.Id).FirstOrCreate(&user).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("✅ Seeder successfully")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/config"
|
||||||
|
service "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/services"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Auth(userService service.UserService, requiredRights ...string) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
authHeader := c.Get("Authorization")
|
||||||
|
token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := utils.VerifyToken(token, config.JWTSecret, config.TokenTypeAccess)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := userService.GetOne(c, userID)
|
||||||
|
if err != nil || user == nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Locals("user", user)
|
||||||
|
|
||||||
|
// if len(requiredRights) > 0 {
|
||||||
|
// userRights, hasRights := config.RoleRights[user.Role]
|
||||||
|
// if (!hasRights || !hasAllRights(userRights, requiredRights)) && c.Params("userId") != userID {
|
||||||
|
// return fiber.NewError(fiber.StatusForbidden, "You don't have permission to access this resource")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func hasAllRights(userRights, requiredRights []string) bool {
|
||||||
|
// rightSet := make(map[string]struct{}, len(userRights))
|
||||||
|
// for _, right := range userRights {
|
||||||
|
// rightSet[right] = struct{}{}
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for _, right := range requiredRights {
|
||||||
|
// if _, exists := rightSet[right]; !exists {
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
jwtware "github.com/gofiber/contrib/jwt"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func JwtConfig() fiber.Handler {
|
||||||
|
return jwtware.New(jwtware.Config{
|
||||||
|
SigningKey: jwtware.SigningKey{Key: []byte("secret")},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LimiterConfig() fiber.Handler {
|
||||||
|
return limiter.New(limiter.Config{
|
||||||
|
Max: 20,
|
||||||
|
Expiration: 15 * time.Minute,
|
||||||
|
LimitReached: func(c *fiber.Ctx) error {
|
||||||
|
return c.Status(fiber.StatusTooManyRequests).
|
||||||
|
JSON(response.Common{
|
||||||
|
Code: fiber.StatusTooManyRequests,
|
||||||
|
Status: "error",
|
||||||
|
Message: "Too many requests, please try again later",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
SkipSuccessfulRequests: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoggerConfig() fiber.Handler {
|
||||||
|
return logger.New(logger.Config{
|
||||||
|
Format: "${time} ${method} ${status} ${path} in ${latency}\n",
|
||||||
|
TimeFormat: "15:04:05.00",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RecoverConfig() fiber.Handler {
|
||||||
|
return recover.New(recover.Config{
|
||||||
|
EnableStackTrace: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package modules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Module interface {
|
||||||
|
RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate)
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/dto"
|
||||||
|
service "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/services"
|
||||||
|
validation "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/validations"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserController struct {
|
||||||
|
UserService service.UserService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserController(userService service.UserService) *UserController {
|
||||||
|
return &UserController{
|
||||||
|
UserService: userService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserController) GetAll(c *fiber.Ctx) error {
|
||||||
|
query := &validation.Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: c.Query("search", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.UserService.GetAll(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.UserListDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get all users successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: dto.ToUserListDTOs(result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserController) GetOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.UserService.GetOne(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get user successfully",
|
||||||
|
Data: dto.ToUserListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserController) CreateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Create)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.UserService.CreateOne(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusCreated,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Create user successfully",
|
||||||
|
Data: dto.ToUserListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserController) UpdateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Update)
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.UserService.UpdateOne(c, req, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Update user successfully",
|
||||||
|
Data: dto.ToUserListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserController) DeleteOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.UserService.DeleteOne(c, uint(id)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Common{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Delete user successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
model "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type UserListDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserDetailDTO struct {
|
||||||
|
UserListDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func ToUserListDTO(m model.User) UserListDTO {
|
||||||
|
return UserListDTO{
|
||||||
|
Id: m.Id,
|
||||||
|
Name: m.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToUserListDTOs(m []model.User) []UserListDTO {
|
||||||
|
result := make([]UserListDTO, len(m))
|
||||||
|
for i, r := range m {
|
||||||
|
result[i] = ToUserListDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package users
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
rUser "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/repositories"
|
||||||
|
sUser "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserModule struct{}
|
||||||
|
|
||||||
|
func (UserModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
UserRoutes(router, userService)
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
model "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/models"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/repository"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserRepository interface {
|
||||||
|
repository.BaseRepository[model.User]
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[model.User]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserRepository(db *gorm.DB) UserRepository {
|
||||||
|
return &UserRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[model.User](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package users
|
||||||
|
|
||||||
|
import (
|
||||||
|
controller "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/controllers"
|
||||||
|
user "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UserRoutes(v1 fiber.Router, s user.UserService) {
|
||||||
|
ctrl := controller.NewUserController(s)
|
||||||
|
|
||||||
|
route := v1.Group("/users")
|
||||||
|
|
||||||
|
route.Get("/", ctrl.GetAll)
|
||||||
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
route.Get("/:id", ctrl.GetOne)
|
||||||
|
route.Patch("/:id", ctrl.UpdateOne)
|
||||||
|
route.Delete("/:id", ctrl.DeleteOne)
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
model "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/models"
|
||||||
|
repository "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/repositories"
|
||||||
|
validation "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/validations"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserService interface {
|
||||||
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]model.User, int64, error)
|
||||||
|
GetOne(ctx *fiber.Ctx, id uint) (*model.User, error)
|
||||||
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*model.User, error)
|
||||||
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*model.User, error)
|
||||||
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type userService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
Repository repository.UserRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserService(repo repository.UserRepository, validate *validator.Validate) UserService {
|
||||||
|
return &userService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
Repository: repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s userService) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.User, int64, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
|
users, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
|
if params.Search != "" {
|
||||||
|
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||||
|
}
|
||||||
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get users: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return users, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s userService) GetOne(c *fiber.Ctx, id uint) (*model.User, error) {
|
||||||
|
user, err := s.Repository.GetByID(c.Context(), id, nil)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed get user by id: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *userService) CreateOne(c *fiber.Ctx, req *validation.Create) (*model.User, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createBody := &model.User{
|
||||||
|
Name: req.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||||
|
s.Log.Errorf("Failed to create user: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s userService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*model.User, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBody := make(map[string]any)
|
||||||
|
|
||||||
|
if req.Name != nil {
|
||||||
|
updateBody["name"] = *req.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to update user: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetOne(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s userService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
|
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to delete user: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
Name string `json:"name" validate:"required_strict,min=3"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query struct {
|
||||||
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BaseRepository[T any] interface {
|
||||||
|
GetAll(ctx context.Context, offset, limit int, modifier func(*gorm.DB) *gorm.DB) ([]T, int64, error)
|
||||||
|
GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*T, error)
|
||||||
|
GetByIDs(ctx context.Context, ids []uint, modifier func(*gorm.DB) *gorm.DB) ([]T, error)
|
||||||
|
|
||||||
|
CreateOne(ctx context.Context, entity *T, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
CreateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
|
||||||
|
UpdateOne(ctx context.Context, id uint, entity *T, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
UpdateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
|
||||||
|
DeleteOne(ctx context.Context, id uint) error
|
||||||
|
DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
|
||||||
|
Upsert(ctx context.Context, entity *T, conflictColumns []clause.Column, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
|
||||||
|
WithTx(tx *gorm.DB) BaseRepository[T]
|
||||||
|
DB() *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseRepositoryImpl[T any] struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBaseRepository[T any](db *gorm.DB) *BaseRepositoryImpl[T] {
|
||||||
|
return &BaseRepositoryImpl[T]{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRepositoryImpl[T]) GetAll(
|
||||||
|
ctx context.Context,
|
||||||
|
offset, limit int,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) ([]T, int64, error) {
|
||||||
|
var entities []T
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
q := r.db.WithContext(ctx).Model(new(T))
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if err := q.Offset(offset).Limit(limit).Find(&entities).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRepositoryImpl[T]) GetByID(
|
||||||
|
ctx context.Context,
|
||||||
|
id uint,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) (*T, error) {
|
||||||
|
entity := new(T)
|
||||||
|
q := r.db.WithContext(ctx)
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
if err := q.First(entity, id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return entity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRepositoryImpl[T]) GetByIDs(
|
||||||
|
ctx context.Context,
|
||||||
|
ids []uint,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) ([]T, error) {
|
||||||
|
var entities []T
|
||||||
|
q := r.db.WithContext(ctx).Model(new(T))
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
if err := q.Where("id IN ?", ids).Find(&entities).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entities) == 0 {
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CREATE ----
|
||||||
|
func (r *BaseRepositoryImpl[T]) CreateOne(
|
||||||
|
ctx context.Context,
|
||||||
|
entity *T,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) error {
|
||||||
|
q := r.db.WithContext(ctx)
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
return q.Create(entity).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRepositoryImpl[T]) CreateMany(
|
||||||
|
ctx context.Context,
|
||||||
|
entities []*T,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) error {
|
||||||
|
q := r.db.WithContext(ctx)
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
return q.Create(&entities).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- UPDATE ----
|
||||||
|
func (r *BaseRepositoryImpl[T]) UpdateOne(
|
||||||
|
ctx context.Context,
|
||||||
|
id uint,
|
||||||
|
entity *T,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) error {
|
||||||
|
q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id)
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := q.Updates(entity)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRepositoryImpl[T]) UpdateMany(
|
||||||
|
ctx context.Context,
|
||||||
|
entities []*T,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) error {
|
||||||
|
q := r.db.WithContext(ctx)
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := q.Save(&entities)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRepositoryImpl[T]) PatchOne(
|
||||||
|
ctx context.Context,
|
||||||
|
id uint,
|
||||||
|
updates map[string]any,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) error {
|
||||||
|
q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id)
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := q.Updates(updates)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- DELETE ----
|
||||||
|
func (r *BaseRepositoryImpl[T]) DeleteOne(ctx context.Context, id uint) error {
|
||||||
|
result := r.db.WithContext(ctx).Delete(new(T), id)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRepositoryImpl[T]) DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error {
|
||||||
|
q := r.db.WithContext(ctx).Model(new(T))
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := q.Delete(new(T))
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- UPSERT ----
|
||||||
|
func (r *BaseRepositoryImpl[T]) Upsert(
|
||||||
|
ctx context.Context,
|
||||||
|
entity *T,
|
||||||
|
conflictColumns []clause.Column,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) error {
|
||||||
|
q := r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||||
|
Columns: conflictColumns,
|
||||||
|
UpdateAll: true,
|
||||||
|
})
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
return q.Create(entity).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRepositoryImpl[T]) WithTx(tx *gorm.DB) BaseRepository[T] {
|
||||||
|
return &BaseRepositoryImpl[T]{db: tx}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BaseRepositoryImpl[T]) DB() *gorm.DB {
|
||||||
|
return r.db
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Error(c *fiber.Ctx, statusCode int, message string, details interface{}) error {
|
||||||
|
var errRes error
|
||||||
|
if details != nil {
|
||||||
|
errRes = c.Status(statusCode).JSON(ErrorDetails{
|
||||||
|
Code: statusCode,
|
||||||
|
Status: "error",
|
||||||
|
Message: message,
|
||||||
|
Errors: details,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
errRes = c.Status(statusCode).JSON(Common{
|
||||||
|
Code: statusCode,
|
||||||
|
Status: "error",
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if errRes != nil {
|
||||||
|
logrus.Errorf("Failed to send error response : %+v", errRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errRes
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
type Common struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Success struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Meta struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
TotalPages int64 `json:"total_pages"`
|
||||||
|
TotalResults int64 `json:"total_results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SuccessWithPaginate[T any] struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Meta Meta `json:"meta"`
|
||||||
|
Data []T `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorDetails struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Errors interface{} `json:"errors"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/validation"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
users "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users"
|
||||||
|
// MODULE IMPORTS
|
||||||
|
)
|
||||||
|
|
||||||
|
func Routes(app *fiber.App, db *gorm.DB) {
|
||||||
|
validate := validation.Validator()
|
||||||
|
api := app.Group("/api")
|
||||||
|
|
||||||
|
// masterRoute.Routes(api, db)
|
||||||
|
|
||||||
|
// root modules di sini
|
||||||
|
allModules := []modules.Module{
|
||||||
|
users.UserModule{},
|
||||||
|
// MODULE REGISTRY
|
||||||
|
}
|
||||||
|
|
||||||
|
// daftarkan root modules
|
||||||
|
for _, m := range allModules {
|
||||||
|
m.RegisterRoutes(api, db, validate)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// FlagType
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
type FlagType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FlagIsActive FlagType = "IS_ACTIVE"
|
||||||
|
)
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Validators
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
func IsValidFlagType(v string) bool {
|
||||||
|
switch FlagType(v) {
|
||||||
|
case FlagIsActive:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// example use
|
||||||
|
|
||||||
|
/**
|
||||||
|
if !utils.IsValidFlagType(req.FlagName) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "invalid flag type")
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/response"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/validation"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ErrorHandler(c *fiber.Ctx, err error) error {
|
||||||
|
if errorsMap := validation.CustomErrorMessages(err); len(errorsMap) > 0 {
|
||||||
|
return response.Error(c, fiber.StatusBadRequest, "Bad Request", errorsMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fiberErr *fiber.Error
|
||||||
|
if errors.As(err, &fiberErr) {
|
||||||
|
return response.Error(c, fiberErr.Code, fiberErr.Message, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Error(c, fiber.StatusInternalServerError, "Internal Server Error", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotFoundHandler(c *fiber.Ctx) error {
|
||||||
|
return response.Error(c, fiber.StatusNotFound, "Endpoint Not Found", nil)
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomFormatter struct {
|
||||||
|
logrus.TextFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
var Log *logrus.Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Log = logrus.New()
|
||||||
|
|
||||||
|
// Set logger to use the custom text formatter
|
||||||
|
Log.SetFormatter(&CustomFormatter{
|
||||||
|
TextFormatter: logrus.TextFormatter{
|
||||||
|
TimestampFormat: "15:04:05.000",
|
||||||
|
FullTimestamp: true,
|
||||||
|
ForceColors: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
Log.SetOutput(os.Stdout)
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type NullString struct {
|
||||||
|
Set bool
|
||||||
|
Value *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NullString) UnmarshalJSON(b []byte) error {
|
||||||
|
ns.Set = true
|
||||||
|
if string(b) == "null" {
|
||||||
|
ns.Value = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(b, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ns.Value = &s
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package secure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Params struct {
|
||||||
|
Memory uint32
|
||||||
|
Time uint32
|
||||||
|
Threads uint8
|
||||||
|
SaltLen uint32
|
||||||
|
KeyLen uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
var Default = &Params{
|
||||||
|
Memory: 64 * 1024,
|
||||||
|
Time: 3,
|
||||||
|
Threads: 2,
|
||||||
|
SaltLen: 16,
|
||||||
|
KeyLen: 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hash(plain string, p *Params) (string, error) {
|
||||||
|
if strings.TrimSpace(plain) == "" {
|
||||||
|
return "", errors.New("empty password")
|
||||||
|
}
|
||||||
|
if p == nil { p = Default }
|
||||||
|
|
||||||
|
salt := make([]byte, p.SaltLen)
|
||||||
|
if _, err := rand.Read(salt); err != nil { return "", err }
|
||||||
|
|
||||||
|
key := argon2.IDKey([]byte(plain), salt, p.Time, p.Memory, p.Threads, p.KeyLen)
|
||||||
|
return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
|
||||||
|
p.Memory, p.Time, p.Threads,
|
||||||
|
base64.RawStdEncoding.EncodeToString(salt),
|
||||||
|
base64.RawStdEncoding.EncodeToString(key),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Verify(encoded, plain string) bool {
|
||||||
|
parts := strings.Split(encoded, "$")
|
||||||
|
if len(parts) != 6 { return false }
|
||||||
|
|
||||||
|
var m uint32; var t uint32; var p uint8
|
||||||
|
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p); err != nil { return false }
|
||||||
|
|
||||||
|
salt, err := base64.RawStdEncoding.DecodeString(parts[4]); if err != nil { return false }
|
||||||
|
want, err := base64.RawStdEncoding.DecodeString(parts[5]); if err != nil { return false }
|
||||||
|
|
||||||
|
got := argon2.IDKey([]byte(plain), salt, t, m, p, uint32(len(want)))
|
||||||
|
return subtle.ConstantTimeCompare(want, got) == 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package secure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RandomToken(n int) (string, error) {
|
||||||
|
// n = bytes, 32 → 256-bit
|
||||||
|
b := make([]byte, n)
|
||||||
|
if _, err := rand.Read(b); err != nil { return "", err }
|
||||||
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package secure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SHA256Hex(s string) string {
|
||||||
|
h := sha256.Sum256([]byte(s))
|
||||||
|
return hex.EncodeToString(h[:])
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func VerifyToken(tokenStr, secret, tokenType string) (uint, error) {
|
||||||
|
token, err := jwt.Parse(tokenStr, func(_ *jwt.Token) (interface{}, error) {
|
||||||
|
return []byte(secret), nil
|
||||||
|
})
|
||||||
|
if err != nil || !token.Valid {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return 0, errors.New("invalid token claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtType, ok := claims["type"].(string)
|
||||||
|
if !ok || jwtType != tokenType {
|
||||||
|
return 0, errors.New("invalid token type")
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, ok := claims["sub"]
|
||||||
|
if !ok {
|
||||||
|
return 0, errors.New("invalid token sub")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := sub.(type) {
|
||||||
|
case float64:
|
||||||
|
return uint(v), nil
|
||||||
|
case string:
|
||||||
|
id, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.New("invalid sub format")
|
||||||
|
}
|
||||||
|
return uint(id), nil
|
||||||
|
default:
|
||||||
|
return 0, errors.New("unsupported sub type")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
reUpper = regexp.MustCompile(`[A-Z]`)
|
||||||
|
reLower = regexp.MustCompile(`[a-z]`)
|
||||||
|
reDigit = regexp.MustCompile(`[0-9]`)
|
||||||
|
reSym = regexp.MustCompile(`[^A-Za-z0-9]`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func Password(fl validator.FieldLevel) bool {
|
||||||
|
pw := fl.Field().String()
|
||||||
|
pw = strings.TrimSpace(pw)
|
||||||
|
|
||||||
|
if len(pw) < 8 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !reUpper.MatchString(pw) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !reLower.MatchString(pw) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !reDigit.MatchString(pw) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !reSym.MatchString(pw) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if strings.Contains(pw, " ") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := fl.Parent()
|
||||||
|
if parent.IsValid() && parent.Kind() == reflect.Struct {
|
||||||
|
emailField := parent.FieldByName("Email")
|
||||||
|
if emailField.IsValid() && emailField.Kind() == reflect.String {
|
||||||
|
if email := emailField.String(); email != "" {
|
||||||
|
if i := strings.IndexByte(email, '@'); i > 0 {
|
||||||
|
local := strings.ToLower(email[:i])
|
||||||
|
if local != "" && strings.Contains(strings.ToLower(pw), local) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequiredStrict(fl validator.FieldLevel) bool {
|
||||||
|
field := fl.Field()
|
||||||
|
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return field.String() != ""
|
||||||
|
case reflect.Ptr:
|
||||||
|
return !field.IsNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
return field.IsValid() && !field.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
func OmitemptyStrict(fl validator.FieldLevel) bool {
|
||||||
|
field := fl.Field()
|
||||||
|
|
||||||
|
if !field.IsValid() || field.IsZero() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Kind() == reflect.String {
|
||||||
|
return field.String() != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
var customMessages = map[string]string{
|
||||||
|
"required": "Field %s is required",
|
||||||
|
"required_strict": "Field %s is required and cannot be null or empty",
|
||||||
|
"omitempty_strict": "Field %s cannot be null or empty when provided",
|
||||||
|
|
||||||
|
"email": "Invalid email address for field %s",
|
||||||
|
"min": "Field %s must have a minimum length of %s characters",
|
||||||
|
"max": "Field %s must have a maximum length of %s characters",
|
||||||
|
"len": "Field %s must be exactly %s characters long",
|
||||||
|
"number": "Field %s must be a number",
|
||||||
|
"positive": "Field %s must be a positive number",
|
||||||
|
"alphanum": "Field %s must contain only alphanumeric characters",
|
||||||
|
"oneof": "Invalid value for field %s",
|
||||||
|
"password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character",
|
||||||
|
}
|
||||||
|
|
||||||
|
func CustomErrorMessages(err error) map[string]string {
|
||||||
|
var validationErrors validator.ValidationErrors
|
||||||
|
if errors.As(err, &validationErrors) {
|
||||||
|
return generateErrorMessages(validationErrors)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateErrorMessages(validationErrors validator.ValidationErrors) map[string]string {
|
||||||
|
errorsMap := make(map[string]string)
|
||||||
|
for _, err := range validationErrors {
|
||||||
|
fieldName := err.StructNamespace()
|
||||||
|
tag := err.Tag()
|
||||||
|
|
||||||
|
customMessage := customMessages[tag]
|
||||||
|
if customMessage != "" {
|
||||||
|
errorsMap[fieldName] = formatErrorMessage(customMessage, err, tag)
|
||||||
|
} else {
|
||||||
|
errorsMap[fieldName] = defaultErrorMessage(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errorsMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {
|
||||||
|
if tag == "min" || tag == "max" || tag == "len" {
|
||||||
|
return fmt.Sprintf(customMessage, err.Field(), err.Param())
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(customMessage, err.Field())
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultErrorMessage(err validator.FieldError) string {
|
||||||
|
return fmt.Sprintf("Field validation for '%s' failed on the '%s' tag", err.Field(), err.Tag())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Validator() *validator.Validate {
|
||||||
|
validate := validator.New()
|
||||||
|
|
||||||
|
if err := validate.RegisterValidation("password", Password); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := validate.RegisterValidation("required_strict", RequiredStrict); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := validate.RegisterValidation("omitempty_strict", OmitemptyStrict); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return validate
|
||||||
|
}
|
||||||
+263
@@ -0,0 +1,263 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Data struct {
|
||||||
|
FeatName string // input full feature (ex: "master/area")
|
||||||
|
Parts []string // split parts ["master","area"]
|
||||||
|
Entity string // last ("area")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
log.Fatal("usage: make gen feat=<feature> (ex: customer | master/area)")
|
||||||
|
}
|
||||||
|
|
||||||
|
feat := os.Args[1]
|
||||||
|
parts := strings.Split(feat, "/")
|
||||||
|
entity := parts[len(parts)-1]
|
||||||
|
|
||||||
|
d := Data{
|
||||||
|
FeatName: feat,
|
||||||
|
Parts: parts,
|
||||||
|
Entity: entity,
|
||||||
|
}
|
||||||
|
|
||||||
|
// daftar template yang mau diproses
|
||||||
|
files := []struct {
|
||||||
|
TplPath string
|
||||||
|
OutDir string
|
||||||
|
OutSuffix string
|
||||||
|
TplName string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
TplPath: "tools/templates/model.tmpl",
|
||||||
|
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s", "models"),
|
||||||
|
OutSuffix: ".model.go",
|
||||||
|
TplName: "model",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TplPath: "tools/templates/validation.tmpl",
|
||||||
|
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s", "validations"),
|
||||||
|
OutSuffix: ".validation.go",
|
||||||
|
TplName: "validation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TplPath: "tools/templates/service.tmpl",
|
||||||
|
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s", "services"),
|
||||||
|
OutSuffix: ".service.go",
|
||||||
|
TplName: "service",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TplPath: "tools/templates/controller.tmpl",
|
||||||
|
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s", "controllers"),
|
||||||
|
OutSuffix: ".controller.go",
|
||||||
|
TplName: "controller",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TplPath: "tools/templates/repository.tmpl",
|
||||||
|
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s", "repositories"),
|
||||||
|
OutSuffix: ".repository.go",
|
||||||
|
TplName: "repository",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TplPath: "tools/templates/dto.tmpl",
|
||||||
|
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s", "dto"),
|
||||||
|
OutSuffix: ".dto.go",
|
||||||
|
TplName: "dto",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TplPath: "tools/templates/route.tmpl",
|
||||||
|
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s"),
|
||||||
|
OutSuffix: "",
|
||||||
|
TplName: "route",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TplPath: "tools/templates/module.tmpl",
|
||||||
|
OutDir: filepath.Join("internal", "modules", toKebabPath(d.Parts[:len(d.Parts)-1]), toKebab(d.Entity)+"s"),
|
||||||
|
OutSuffix: "",
|
||||||
|
TplName: "module",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
// pastikan template ketemu
|
||||||
|
if _, err := os.Stat(file.TplPath); err != nil {
|
||||||
|
log.Fatalf("template not found at %s: %v", file.TplPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse template
|
||||||
|
tpl := template.Must(
|
||||||
|
template.New(file.TplName).
|
||||||
|
Funcs(template.FuncMap{
|
||||||
|
"Pascal": toPascalCase,
|
||||||
|
"Camel": toCamelCase,
|
||||||
|
"Plural": toPlural,
|
||||||
|
"Kebab": toKebab,
|
||||||
|
}).
|
||||||
|
ParseFiles(file.TplPath),
|
||||||
|
)
|
||||||
|
|
||||||
|
// pastikan folder ada
|
||||||
|
if err := os.MkdirAll(file.OutDir, 0o755); err != nil {
|
||||||
|
log.Fatalf("make dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nama file output
|
||||||
|
var outFile string
|
||||||
|
switch file.TplName {
|
||||||
|
case "route":
|
||||||
|
outFile = filepath.Join(file.OutDir, "route.go")
|
||||||
|
case "module":
|
||||||
|
outFile = filepath.Join(file.OutDir, "module.go")
|
||||||
|
default:
|
||||||
|
outFile = filepath.Join(file.OutDir, strings.ToLower(d.Entity)+file.OutSuffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hindari overwrite
|
||||||
|
if _, err := os.Stat(outFile); err == nil {
|
||||||
|
log.Fatalf("file already exists: %s", outFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(outFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("create file: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err := tpl.ExecuteTemplate(f, file.TplName, d); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Generated:", outFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMainRoute(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMainRoute(d Data) {
|
||||||
|
routeFile := "internal/route/route.go"
|
||||||
|
content, err := os.ReadFile(routeFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("skip update route.go: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// entity & path
|
||||||
|
modPath := filepath.Join(append(toCamelParts(d.Parts[:len(d.Parts)-1]), toCamelCase(d.Entity)+"s")...)
|
||||||
|
modName := toCamelCase(d.Entity) + "s"
|
||||||
|
pkgName := toPascalCase(d.Entity) + "Module"
|
||||||
|
|
||||||
|
// Inject import
|
||||||
|
importLine := fmt.Sprintf("\t%[1]s \"%s/internal/modules/%s\"", modName, "github.com/hafizhproject45/Golang-Boilerplate.git", modPath)
|
||||||
|
if !strings.Contains(string(content), importLine) {
|
||||||
|
content = []byte(strings.Replace(string(content),
|
||||||
|
"// MODULE IMPORTS",
|
||||||
|
importLine+"\n\t// MODULE IMPORTS",
|
||||||
|
1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject registry
|
||||||
|
registryLine := fmt.Sprintf("\t\t%[1]s.%[2]s{},", modName, pkgName)
|
||||||
|
if !strings.Contains(string(content), registryLine) {
|
||||||
|
content = []byte(strings.Replace(string(content),
|
||||||
|
"// MODULE REGISTRY",
|
||||||
|
registryLine+"\n\t\t// MODULE REGISTRY",
|
||||||
|
1))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(routeFile, content, 0644); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("Updated:", routeFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPascalCase(s string) string {
|
||||||
|
sep := func(r rune) bool { return r == '_' || r == '-' || r == ' ' || r == '/' }
|
||||||
|
parts := strings.FieldsFunc(s, sep)
|
||||||
|
for i, p := range parts {
|
||||||
|
if p == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts[i] = strings.ToUpper(p[:1]) + strings.ToLower(p[1:])
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCamelCase(s string) string {
|
||||||
|
p := toPascalCase(s)
|
||||||
|
if p == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.ToLower(p[:1]) + p[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// simple pluralizer (cukup untuk kasus umum: tambah 's')
|
||||||
|
func toPlural(s string) string {
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
if strings.HasSuffix(s, "y") && len(s) > 1 {
|
||||||
|
prev := s[len(s)-2]
|
||||||
|
if !(prev == 'a' || prev == 'i' || prev == 'u' || prev == 'e' || prev == 'o') {
|
||||||
|
return s[:len(s)-1] + "ies"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s + "s"
|
||||||
|
}
|
||||||
|
|
||||||
|
// kebab-case (untuk folder)
|
||||||
|
func toKebab(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, "_", "-")
|
||||||
|
var b strings.Builder
|
||||||
|
for i, r := range s {
|
||||||
|
if r >= 'A' && r <= 'Z' {
|
||||||
|
if i > 0 {
|
||||||
|
b.WriteByte('-')
|
||||||
|
}
|
||||||
|
b.WriteRune(r + 32)
|
||||||
|
} else {
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out := b.String()
|
||||||
|
out = strings.ReplaceAll(out, "--", "-")
|
||||||
|
return strings.Trim(out, "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
// join multiple parts jadi kebab path
|
||||||
|
func toKebabPath(parts []string) string {
|
||||||
|
return filepath.Join(toKebabParts(parts)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toKebabParts(parts []string) []string {
|
||||||
|
var out []string
|
||||||
|
for _, p := range parts {
|
||||||
|
out = append(out, toKebab(p))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// join multiple parts jadi camelCase path
|
||||||
|
// func toCamelPath(parts []string) string {
|
||||||
|
// return filepath.Join(toCamelParts(parts)...)
|
||||||
|
// }
|
||||||
|
|
||||||
|
func toCamelParts(parts []string) []string {
|
||||||
|
var out []string
|
||||||
|
for i, p := range parts {
|
||||||
|
if i == 0 {
|
||||||
|
// part pertama lower-case semua
|
||||||
|
out = append(out, toCamelCase(p))
|
||||||
|
} else {
|
||||||
|
// part berikutnya PascalCase biar tetap nyambung camel
|
||||||
|
out = append(out, toPascalCase(p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
{{define "controller"}}package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/{{Kebab .FeatName}}s/dto"
|
||||||
|
service "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/{{Kebab .FeatName}}s/services"
|
||||||
|
validation "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/{{Kebab .FeatName}}s/validations"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type {{Pascal .Entity}}Controller struct {
|
||||||
|
{{Pascal .Entity}}Service service.{{Pascal .Entity}}Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func New{{Pascal .Entity}}Controller({{Camel .Entity}}Service service.{{Pascal .Entity}}Service) *{{Pascal .Entity}}Controller {
|
||||||
|
return &{{Pascal .Entity}}Controller{
|
||||||
|
{{Pascal .Entity}}Service: {{Camel .Entity}}Service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *{{Pascal .Entity}}Controller) GetAll(c *fiber.Ctx) error {
|
||||||
|
query := &validation.Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: c.Query("search", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.{{Pascal .Entity}}Service.GetAll(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.{{Pascal .Entity}}ListDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get all {{Camel .Entity}}s successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: dto.To{{Pascal .Entity}}ListDTOs(result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *{{Pascal .Entity}}Controller) GetOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.{{Pascal .Entity}}Service.GetOne(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get {{Camel .Entity}} successfully",
|
||||||
|
Data: dto.To{{Pascal .Entity}}ListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *{{Pascal .Entity}}Controller) CreateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Create)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.{{Pascal .Entity}}Service.CreateOne(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusCreated,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Create {{Camel .Entity}} successfully",
|
||||||
|
Data: dto.To{{Pascal .Entity}}ListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *{{Pascal .Entity}}Controller) UpdateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Update)
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.{{Pascal .Entity}}Service.UpdateOne(c, req, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Update {{Camel .Entity}} successfully",
|
||||||
|
Data: dto.To{{Pascal .Entity}}ListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *{{Pascal .Entity}}Controller) DeleteOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.{{Pascal .Entity}}Service.DeleteOne(c, uint(id)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Common{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Delete {{Camel .Entity}} successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{{define "dto"}}package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
model "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/{{Kebab .FeatName}}s/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type {{Pascal .Entity}}ListDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type {{Pascal .Entity}}DetailDTO struct {
|
||||||
|
{{Pascal .Entity}}ListDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func To{{Pascal .Entity}}ListDTO(m model.{{Pascal .Entity}}) {{Pascal .Entity}}ListDTO {
|
||||||
|
return {{Pascal .Entity}}ListDTO{
|
||||||
|
Id: m.Id,
|
||||||
|
Name: m.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func To{{Pascal .Entity}}ListDTOs(m []model.{{Pascal .Entity}}) []{{Pascal .Entity}}ListDTO {
|
||||||
|
result := make([]{{Pascal .Entity}}ListDTO, len(m))
|
||||||
|
for i, r := range m {
|
||||||
|
result[i] = To{{Pascal .Entity}}ListDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{{define "model"}}package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type {{Pascal .Entity}} struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{{define "module"}}package {{Kebab .Entity}}s
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
r{{Pascal .Entity}} "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/{{Kebab .FeatName}}s/repositories"
|
||||||
|
s{{Pascal .Entity}} "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/{{Kebab .FeatName}}s/services"
|
||||||
|
|
||||||
|
rUser "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/repositories"
|
||||||
|
sUser "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type {{Pascal .Entity}}Module struct{}
|
||||||
|
|
||||||
|
func ({{Pascal .Entity}}Module) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
{{Camel .Entity}}Repo := r{{Pascal .Entity}}.New{{Pascal .Entity}}Repository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
|
{{Camel .Entity}}Service := s{{Pascal .Entity}}.New{{Pascal .Entity}}Service({{Camel .Entity}}Repo, validate)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
{{Pascal .Entity}}Routes(router, userService, {{Camel .Entity}}Service)
|
||||||
|
}
|
||||||
|
|
||||||
|
{{end}}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{{define "repository"}}package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
model "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/{{Kebab .FeatName}}s/models"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/repository"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type {{Pascal .Entity}}Repository interface {
|
||||||
|
repository.BaseRepository[model.{{Pascal .Entity}}]
|
||||||
|
}
|
||||||
|
|
||||||
|
type {{Pascal .Entity}}RepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[model.{{Pascal .Entity}}]
|
||||||
|
}
|
||||||
|
|
||||||
|
func New{{Pascal .Entity}}Repository(db *gorm.DB) {{Pascal .Entity}}Repository {
|
||||||
|
return &{{Pascal .Entity}}RepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[model.{{Pascal .Entity}}](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{{define "route"}}package {{Kebab .Entity}}s
|
||||||
|
|
||||||
|
import (
|
||||||
|
m "github.com/hafizhproject45/Golang-Boilerplate.git/internal/middleware"
|
||||||
|
controller "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/{{Kebab .FeatName}}s/controllers"
|
||||||
|
{{Camel .Entity}} "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/{{Kebab .FeatName}}s/services"
|
||||||
|
user "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func {{Pascal .Entity}}Routes(v1 fiber.Router, u user.UserService, s {{Camel .Entity}}.{{Pascal .Entity}}Service) {
|
||||||
|
ctrl := controller.New{{Pascal .Entity}}Controller(s)
|
||||||
|
|
||||||
|
route := v1.Group("/{{Kebab .Entity}}s")
|
||||||
|
|
||||||
|
route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||||
|
route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
|
route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||||
|
route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||||
|
route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
{{define "service"}}package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
model "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/{{Kebab .FeatName}}s/models"
|
||||||
|
repository "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/{{Kebab .FeatName}}s/repositories"
|
||||||
|
validation "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/{{Kebab .FeatName}}s/validations"
|
||||||
|
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type {{Pascal .Entity}}Service interface {
|
||||||
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]model.{{Pascal .Entity}}, int64, error)
|
||||||
|
GetOne(ctx *fiber.Ctx, id uint) (*model.{{Pascal .Entity}}, error)
|
||||||
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*model.{{Pascal .Entity}}, error)
|
||||||
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*model.{{Pascal .Entity}}, error)
|
||||||
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type {{Camel .Entity}}Service struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
Repository repository.{{Pascal .Entity}}Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func New{{Pascal .Entity}}Service(repo repository.{{Pascal .Entity}}Repository, validate *validator.Validate) {{Pascal .Entity}}Service {
|
||||||
|
return &{{Camel .Entity}}Service{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
Repository: repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s {{Camel .Entity}}Service) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.{{Pascal .Entity}}, int64, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
|
{{Camel .Entity}}s, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
|
if params.Search != "" {
|
||||||
|
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||||
|
}
|
||||||
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get {{Camel .Entity}}s: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return {{Camel .Entity}}s, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s {{Camel .Entity}}Service) GetOne(c *fiber.Ctx, id uint) (*model.{{Pascal .Entity}}, error) {
|
||||||
|
{{Camel .Entity}}, err := s.Repository.GetByID(c.Context(), id, nil)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "{{Pascal .Entity}} not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed get {{Camel .Entity}} by id: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return {{Camel .Entity}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *{{Camel .Entity}}Service) CreateOne(c *fiber.Ctx, req *validation.Create) (*model.{{Pascal .Entity}}, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createBody := &model.{{Pascal .Entity}}{
|
||||||
|
Name: req.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||||
|
s.Log.Errorf("Failed to create {{Camel .Entity}}: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s {{Camel .Entity}}Service) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*model.{{Pascal .Entity}}, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBody := make(map[string]any)
|
||||||
|
|
||||||
|
if req.Name != nil {
|
||||||
|
updateBody["name"] = *req.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "{{Pascal .Entity}} not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to update {{Camel .Entity}}: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetOne(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s {{Camel .Entity}}Service) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
|
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "{{Pascal .Entity}} not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to delete {{Camel .Entity}}: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{{define "validation"}}package validation
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
Name string `json:"name" validate:"required_strict,min=3"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query struct {
|
||||||
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
Reference in New Issue
Block a user