diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..32270b2 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,55 @@ +# ============================================================================== +# APPLICATION +# ============================================================================== +APP_ENV=development +SERVER_PORT=8080 +SERVER_READ_TIMEOUT=10s +SERVER_WRITE_TIMEOUT=10s + +# ============================================================================== +# DATABASE (PostgreSQL) +# ============================================================================== +DB_HOST=localhost +DB_PORT=5432 +DB_USER=aurganize +DB_PASSWORD=aurganize_dev_pass_change_in_production +DB_NAME=aurganize_v62 +DB_SSLMODE=disable + +# Connection Pool +DB_MAX_OPEN_CONNS=25 +DB_MAX_IDLE_CONNS=5 +DB_CONN_MAX_LIFETIME=5m + +# ============================================================================== +# JWT AUTHENTICATION +# ============================================================================== +# IMPORTANT: Change these secrets in production! +# Generate with: openssl rand -base64 32 +JWT_ACCESS_SECRET=your-super-secret-access-key-min-32-chars-change-in-production +JWT_REFRESH_SECRET=your-super-secret-refresh-key-min-32-chars-must-be-different +JWT_ACCESS_EXPIRY=15m +JWT_REFRESH_EXPIRY=168h +JWT_ISSUER=aurganize-v62 + +# ============================================================================== +# REDIS (Caching & Sessions) +# ============================================================================== +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=0 + +# ============================================================================== +# NATS (Event Messaging) +# ============================================================================== +NATS_URL=nats://localhost:4222 + +# ============================================================================== +# MINIO (S3-Compatible Storage) +# ============================================================================== +MINIO_ENDPOINT=localhost:9000 +MINIO_ACCESS_KEY=minioadmin +MINIO_SECRET_KEY=minioadmin +MINIO_BUCKET=aurganize +MINIO_USE_SSL=false \ No newline at end of file diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go new file mode 100644 index 0000000..0c40d87 --- /dev/null +++ b/backend/cmd/api/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Aurganize") +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..258d0b3 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,46 @@ +module github.com/creativenoz/aurganize-v62/backend + +go 1.25.2 + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.6 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/labstack/echo/v4 v4.13.4 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/nats-io/nats.go v1.47.0 // indirect + github.com/nats-io/nkeys v0.4.11 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/redis/go-redis/v9 v9.17.1 // indirect + github.com/rs/zerolog v1.34.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.11.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/postgres v1.6.0 // indirect + gorm.io/gorm v1.31.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..128d8aa --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,103 @@ +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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/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/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +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.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +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/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-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= +github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +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/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= +github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= +github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.17.1 h1:7tl732FjYPRT9H9aNfyTwKg9iTETjWjGKEJ2t/5iWTs= +github.com/redis/go-redis/v9 v9.17.1/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +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/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 0000000..036a102 --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,209 @@ +package config + +import ( + "fmt" + "os" + "strconv" + "time" + + "github.com/joho/godotenv" +) + +type Config struct { + Server ServerConfig + Database DatabaseConfig + JWT JWTConfig + Redis RedisConfig + NATS NATSConfig + Storage StorageConfig +} + +// ServerConfig type holds the information about the http server settings +type ServerConfig struct { + Port string // HTTP port to listen on + Environment string // can be development, staging, production + ReadTimeout time.Duration // Max time to read request + WriteTimeout time.Duration // Max time to write response +} + +// DatabaseConfig contains postgresSQL connection settings +type DatabaseConfig struct { + Host string // Database host + Port string // Database port + User string // Database user + Password string // Database password + DBName string // Database name + SSLMode string // SSL mode : disable, require, verify-full ? not sure what this field is set for + MaxOpenConns int // Maximum open connections + MaxIdleConns int // Maximum idle connections + ConnMaxLifetime time.Duration // Maximum connection lifetime +} + +// JWT Config contains JWT token settings +type JWTConfig struct { + AccessSecret string // Secret for access tokens + RefreshSecret string // Secret for refresh tokens + AccessExpiry time.Duration // Accees token expiry (15 minutes) + RefreshExpiry time.Duration // Refresh token expiry (7 days) + Issuer string // Token issuer claim +} + +type RedisConfig struct { + Host string // Redis host + Port string // Redis port + Password string // Redis password (set to empty if no auth is set) + DB int // Redis database number +} + +// NATSConfig contains NATS messaging settings +type NATSConfig struct { + URL string // NATS server URL +} + +// StorageCongfig contains MinIO (s3) settings +type StorageConfig struct { + Endpoint string // MinIO endpoint + AccessKeyID string // Access key + SecretAccessKey string // Secret key + BucketName string // Bucket name + UseSSL bool // User HTTPS +} + +func Load() (*Config, error) { + if os.Getenv("APP_ENV") != "production" { + if err := godotenv.Load(); err != nil { + fmt.Println("Warning: .env file not found, using environment variables") + } + } + + cfg := &Config{ + Server: ServerConfig{ + Port: getEnv("SERVER_PORT", "8080"), + Environment: getEnv("APP_ENV", "development"), + ReadTimeout: parseDuration(getEnv("SERVER_READ_TIMEOUT", "10s")), + WriteTimeout: parseDuration(getEnv("SERVER_WRITE_TIMEOUT", "10s")), + }, + Database: DatabaseConfig{ + Host: getEnv("DB_HOST", "localhost"), + Port: getEnv("DB_PORT", "5432"), + User: getEnv("DB_USER", "aurganize"), + Password: getEnv("DB_PASSWORD", ""), + DBName: getEnv("DB_NAME", "aruganize_db_1"), + SSLMode: getEnv("DB_SSLMODE", "disable"), + MaxOpenConns: parseInt(getEnv("DB_MAX_OPEN_CONNECTIONS", "25")), + MaxIdleConns: parseInt(getEnv("DB_MAX_IDLE_CONNECTIONS", "5")), + ConnMaxLifetime: parseDuration(getEnv("DB_CONNECTION_MAX_LIFETIME", "5m")), + }, + + JWT: JWTConfig{ + AccessSecret: getEnv("JWT_ACCESS_SECRET", ""), + RefreshSecret: getEnv("JWT_REFRESH_SECRET", ""), + AccessExpiry: parseDuration(getEnv("JWT_ACCESS_EXPIRY", "15m")), + RefreshExpiry: parseDuration(getEnv("JWT_REFRESH_EXPIRY", "168h")), + Issuer: getEnv("JWT_ISSUER", "aurganize-v62"), + }, + Redis: RedisConfig{ + Host: getEnv("REDIST_HOST", "localhost"), + Port: getEnv("REDIS_PORT", "6379"), + Password: getEnv("REDIS_PASSWORD", ""), + DB: parseInt(getEnv("REDIS_DB", "0")), + }, + NATS: NATSConfig{ + URL: getEnv("NATS_URL", "nats://localhost:4222"), + }, + Storage: StorageConfig{ + Endpoint: getEnv("MINIO_ENDPOINT", "localhost:9000"), + AccessKeyID: getEnv("MINIO_ACCESS_KEY", "minioadmin"), + SecretAccessKey: getEnv("MINIO_SECRET_KEY", "miniosecretkey"), + BucketName: getEnv("MINIO_BUCKET", "aurganize_bucket_1"), + UseSSL: parseBool(getEnv("MINIO_USE_SSL", "false")), + }, + } + if err := cfg.Validate(); err != nil { + return nil, fmt.Errorf("configuration validation failure [%w]", err) + } + + return cfg, nil +} + +// Validate checks if all required configuration is present and valid +func (c *Config) Validate() error { + // Database password required in production + if c.Database.Password == "" && c.Server.Environment == "production" { + return fmt.Errorf("DB_PASSWORD is required in production") + } + // JWT secrets are required always + if c.JWT.AccessSecret == "" { + return fmt.Errorf("JWT_ACCESS_SECRET is required") + } + + if c.JWT.RefreshSecret == "" { + return fmt.Errorf("JWT_REFRESH_SECRET is required") + } + // JWT secrets should be different + if c.JWT.AccessSecret == c.JWT.RefreshSecret { + return fmt.Errorf("JWT_ACCESS_SECRET and JWT_REFRESH_SECRET must be different") + } + + validEnvs := map[string]bool{ + "development": true, + "test": true, + "staging": true, + "UAT": true, + "production": true, + } + if !validEnvs[c.Server.Environment] { + return fmt.Errorf("invalid environment configured in enviroment") + } + return nil +} + +// Helper Functions +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +func parseDuration(s string) time.Duration { + d, err := time.ParseDuration(s) + if err != nil { + return 0 + } + return d +} + +func parseInt(s string) int { + i, err := strconv.Atoi(s) + if err != nil { + return 0 + } + return i +} + +func parseBool(s string) bool { + b, err := strconv.ParseBool(s) + if err != nil { + return false + } + return b +} + +// DatabaseDSN returns the PostgreSQL connection string +func (c *Config) DatabaseDSN() string { + return fmt.Sprintf( + "host=%s post %s user=%s password=%s dbname=%s sslmode=%s", + c.Database.Host, + c.Database.Port, + c.Database.User, + c.Database.Password, + c.Database.DBName, + c.Database.SSLMode, + ) +} + +// RedisDSN returns the Redis connection string +func (c *Config) RedisDSN() string { + return fmt.Sprintf("%s:%s", c.Redis.Host, c.Redis.Port) +} diff --git a/backend/pkg/logger/examples_test.go b/backend/pkg/logger/examples_test.go new file mode 100644 index 0000000..3f857a9 --- /dev/null +++ b/backend/pkg/logger/examples_test.go @@ -0,0 +1,107 @@ +package logger_test + +import ( + "time" + + "github.com/creativenoz/aurganize-v62/backend/pkg/logger" + "github.com/rs/zerolog/log" +) + +// Example_basicUsage demonstrates basic logger usage +func Example_basicUsage() { + // Initialize logger + logger.Init("development") + + // Log at different levels + log.Debug().Msg("This is debug information") + log.Info().Msg("This is informational") + log.Warn().Msg("This is a warning") + log.Error().Msg("This is an error") + + // Output depends on log level +} + +// Example_structuredLogging demonstrates structured logging with fields +func Example_structuredLogging() { + logger.Init("development") + + // Log with structured fields + log.Info(). + Str("user_id", "12345"). + Str("action", "login"). + Bool("success", true). + Dur("duration", 150*time.Millisecond). + Msg("User login attempt") + + // JSON output: + // { + // "level": "info", + // "user_id": "12345", + // "action": "login", + // "success": true, + // "duration": 150, + // "message": "User login attempt" + // } +} + +// Example_contextLogger demonstrates creating logger with context +func Example_contextLogger() { + logger.Init("development") + + // Create logger with request context + requestLogger := logger.WithContext(map[string]interface{}{ + "request_id": "req-abc-123", + "user_id": "user-456", + "ip": "192.168.1.1", + }) + + // All logs from this logger include context + requestLogger.Info().Msg("Request started") + requestLogger.Info().Msg("Processing payment") + requestLogger.Info().Msg("Request completed") + + // All three logs include request_id, user_id, and ip +} + +// Example_errorLogging demonstrates logging errors +func Example_errorLogging() { + logger.Init("development") + + // Simulate an error + err := someFunction() + if err != nil { + log.Error(). + Err(err). // Add error + Str("user_id", "123"). + Str("operation", "database_query"). + Msg("Failed to fetch user") + } +} + +// Example_subLogger demonstrates module-specific loggers +func Example_subLogger() { + logger.Init("development") + + // Create a sub-logger for authentication module + authLogger := log.With(). + Str("module", "auth"). + Str("version", "v1"). + Logger() + + authLogger.Info().Msg("Auth module initialized") + authLogger.Debug().Msg("Loading auth configuration") + + // Create a sub-logger for database module + dbLogger := log.With(). + Str("module", "database"). + Logger() + + dbLogger.Info().Msg("Database connection established") + + // Now you can filter logs by module in production +} + +// Helper function for example +func someFunction() error { + return nil +} diff --git a/backend/pkg/logger/logger.go b/backend/pkg/logger/logger.go new file mode 100644 index 0000000..4d83dca --- /dev/null +++ b/backend/pkg/logger/logger.go @@ -0,0 +1,208 @@ +package logger + +import ( + "io" + "os" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +const ( + LevelDebug = "debug" + LevelInfo = "info" + LevelWarn = "warn" + LevelError = "error" +) + +// Init initializes the global logger with environment-specific settings. +// Call this once at application startup. +// +// Environment determines output format: +// - development: Pretty console output with colors +// - staging/production: JSON output for log aggregation +// +// Example: +// +// logger.Init("development") +// log.Info().Msg("Application started") +func Init(enviroment string) { + + // Configuring the time format for loggin + // Unix style timestamp in production for efficiency + // Humar-readable in development + if enviroment == "production" { + // This is the unixtimestamp format used in production for efficiency : 1732632645 + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + } else { + zerolog.TimeFieldFormat = time.RFC3339 + // This is format for human readable format : 2025-11-26T10:30:45-05:00 + } + + var output io.Writer = os.Stdout + + if enviroment == "development" { + output = zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, // 2025-11-26T10:30:45-05:00 + NoColor: false, // Enabling Colours + } + } + + // Production uses default JSON output to stdout + + // Set global logger + // Caller() : this caused adtional overhead as runtime.Caller() is called, which is worth + // for debugging value, should disable in production + log.Logger = zerolog.New(output). + With(). + Timestamp(). + Caller(). // This line is to add file and line number information into the log + Logger() + + // set global log level + // Debug (most verbose) + // ↓ + // Info + // ↓ + // Warn + // ↓ + // Error + // ↓ + // Fatal (least verbose) + switch enviroment { + case "production": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "UAT": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "staging": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "test": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "development": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + } + + // Log initialization + log.Info(). + Str("environment", enviroment). + Str("log_level", zerolog.GlobalLevel().String()). + Msg("logger initialized") +} + +func InitWithLevel(environment, level string) { + Init(environment) + + switch level { + case LevelDebug: + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case LevelInfo: + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case LevelWarn: + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case LevelError: + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + default: + log.Warn(). + Str("provided_level", level). + Str("using_level", zerolog.GlobalLevel().String()). + Msg("Invalid log level, using default") + } + + log.Info(). + Str("environment", environment). + Str("log_level", zerolog.GlobalLevel().String()). + Msg("Logger initialized with custom level") +} + +// GetLogger returns the global logger instance. +// Use this to get a logger with additional context. +// +// Example : +// +// logger := logger.GetLogger(). +// With(). +// Str("module","auth"). +// Logger() +// logger.Info().Msg("Auth module started") +func GetLogger() *zerolog.Logger { + return &log.Logger +} + +// WithContext returns a logger with additional context fields. +// Useful for adding request-specific context. +// +// Example: +// +// contextLogger := logger.WithContext(map[string]interface{}{ +// "request_id": "abc-123", +// "user_id": "user-456", +// }) +// contextLogger.Info().Msg("Processing request") +func WithContext(fields map[string]interface{}) *zerolog.Logger { + logger := log.Logger + for key, value := range fields { + logger = logger.With().Interface(key, value).Logger() + } + + return &logger +} + +// Example Usage log functions (for familiarizing its usage) + +// LogDebug logs debug information (these are verbose, development only) +func ExampleDebug() { + log.Debug(). + Str("function", "ExampleDebug"). + Int("iteration", 1). + Msg("Debug information") +} + +func ExampleInfo() { + log.Info(). + Str("user_id", "123"). + Str("reason", "invalid_token"). + Msg("User logged in successfully") +} + +func ExampleError() { + log.Error(). + Err(nil). // we add the actual error here + Str("user_id", "123"). + Str("operation", "database_query"). + Msg("Failed to fetch user data") +} + +// Log Fatal logs fatal erros and exits the application +// user sparingly - only for unrecoverable errors +func ExmapleFatal() { + // log.Fatal(). + // Err(err). + // Msg("Cannot connect to database") + // -- Application exits after this +} + +// Log with fields demonstrated logging mutliple fields +func ExampleWithFields() { + log.Info(). + Str("user_id", "123"). + Str("email", "user@example.com"). + Int("login_attemtps", 3). + Bool("success", true). + Dur("duration", 150*time.Millisecond). + Msg("login completed") +} + +// Log with SubLogger demonstrates creating sub-loggers +func ExampleSubLogger() { + + // create a sub-logger for a specific module + authLogger := log.With(). + Str("module", "auth"). + Str("version", "v1"). + Logger() + + authLogger.Info().Msg("Auth module initialized") + authLogger.Debug().Msg("Loading auth configuration") +} diff --git a/backend/pkg/logger/logger_test.go b/backend/pkg/logger/logger_test.go new file mode 100644 index 0000000..09b82e8 --- /dev/null +++ b/backend/pkg/logger/logger_test.go @@ -0,0 +1,212 @@ +package logger + +import ( + "bytes" + "testing" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" +) + +// TestInit_Developement tests logger initialization in development mode +func TestInit_Development(t *testing.T) { + // Arrange + environment := "development" + + // Act + Init(environment) + + // Assert + assert.Equal(t, zerolog.DebugLevel, zerolog.GlobalLevel(), "Development should use Debug level") +} + +// TestInit_Production tests logger initialization in production mode +func TestInit_Production(t *testing.T) { + // Arrange + environment := "production" + + // Act + Init(environment) + + // Assert + assert.Equal(t, zerolog.InfoLevel, zerolog.GlobalLevel(), "Production should use Info level") +} + +func TestInitWithLevel(t *testing.T) { + tests := []struct { + name string + environment string + level string + expectedLevel zerolog.Level + }{ + { + name: "Debug level", + environment: "production", + level: LevelDebug, + expectedLevel: zerolog.DebugLevel, + }, + { + name: "Info level", + environment: "production", + level: LevelInfo, + expectedLevel: zerolog.InfoLevel, + }, + { + name: "Warn level", + environment: "production", + level: LevelWarn, + expectedLevel: zerolog.WarnLevel, + }, + { + name: "Error level", + environment: "production", + level: LevelError, + expectedLevel: zerolog.ErrorLevel, + }, + { + name: "Invalid level defaults to Info", + environment: "production", + level: "invalid", + expectedLevel: zerolog.InfoLevel, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Act + InitWithLevel(test.environment, test.level) + + // Assert + assert.Equal(t, test.expectedLevel, zerolog.GlobalLevel()) + }) + } +} + +// TestGetLogger tests getting logger instance +func TestGetLogger(t *testing.T) { + // Arrage + Init("development") + // Act + logger := GetLogger() + + // Assert + assert.NotNil(t, logger, "GetLogger should return non-nil logger") +} + +// TestWithContext tests logging with additional context +func TestWithContext(t *testing.T) { + // Arrange + var buff bytes.Buffer + log.Logger = zerolog.New(&buff) + + fields := map[string]interface{}{ + "request_id": "test-123", + "user_id": "user-456", + } + + // Act + logger := WithContext(fields) + logger.Info().Msg("Test message") + + // Assert + output := buff.String() + assert.Contains(t, output, "test-123", "should include the request_id") + assert.Contains(t, output, "user-456", "should include the user_id") + assert.Contains(t, output, "Test message", "should include message") +} + +// TestLogLevels tests that log levels fitler correctly +func TestLogLevels(t *testing.T) { + tests := []struct { + name string + setLevel zerolog.Level + logLevel zerolog.Level + shouldAppear bool + }{ + { + name: "Debug message appears when level is Debug", + setLevel: zerolog.DebugLevel, + logLevel: zerolog.DebugLevel, + shouldAppear: true, + }, + { + name: "Debug message hidden when level is Info", + setLevel: zerolog.InfoLevel, + logLevel: zerolog.DebugLevel, + shouldAppear: false, + }, + { + name: "Error message appears when level is Info", + setLevel: zerolog.InfoLevel, + logLevel: zerolog.ErrorLevel, + shouldAppear: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Arrange + var buff bytes.Buffer + zerolog.SetGlobalLevel(test.setLevel) + log.Logger = zerolog.New(&buff) + + // Act + switch test.logLevel { + case zerolog.DebugLevel: + log.Debug().Msg("test message") + case zerolog.InfoLevel: + log.Info().Msg("test message") + case zerolog.ErrorLevel: + log.Error().Msg("test message") + } + + // Assert + output := buff.String() + if test.shouldAppear { + assert.Contains(t, output, "test message") + } else { + assert.Empty(t, output) + } + }) + } + +} + +// BenchmarkLogger benchmarks logger performance +func BenchmarkLogger(b *testing.B) { + // Setup + var buff bytes.Buffer + log.Logger = zerolog.New(&buff) + + b.ResetTimer() + + // Run Benchmark + + for i := 0; i < b.N; i++ { + log.Info(). + Str("key", "value"). + Int("number", 42). + Msg("Benchmark message") + } +} + +// Benchmark Logger with multiple fields +func BenchmarkLoggerWithFields(b *testing.B) { + // Setup + var buff bytes.Buffer + log.Logger = zerolog.New(&buff) + + b.ResetTimer() + + // Run Benchmark + for i := 0; i < b.N; i++ { + log.Info(). + Str("request_id", "abc-123"). + Str("user_id", "user-456"). + Str("method", "POST"). + Str("path", "/api/v1/users"). + Int("status", 200). + Dur("duration", 150). + Msg("Request completed") + } +}