From afc01d06636e4a382f277971067e7ad7176dd6f3 Mon Sep 17 00:00:00 2001 From: Mosazghi Date: Sat, 18 Apr 2026 01:05:36 +0200 Subject: [PATCH 1/3] Migreate from gin to net/http --- go.mod | 26 ---------- go.sum | 70 ------------------------- internal/handler/env_var_handler.go | 79 ++++++++++++++++------------- internal/handler/project_handler.go | 68 ++++++++++++------------- internal/handler/utils.go | 32 ++++++++++-- internal/router/middlewares.go | 67 ++++++++++++------------ internal/router/middlewares_test.go | 15 ++---- internal/router/router.go | 56 ++++++++------------ 8 files changed, 165 insertions(+), 248 deletions(-) diff --git a/go.mod b/go.mod index 34a7adb..34bb7f5 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module env-manager go 1.25.6 -require github.com/gin-gonic/gin v1.12.0 - require ( github.com/bytedance/gopkg v0.1.3 github.com/glebarez/sqlite v1.11.0 @@ -16,46 +14,22 @@ require ( ) require ( - github.com/bytedance/sonic v1.15.0 // indirect - github.com/bytedance/sonic/loader v0.5.0 // indirect - github.com/cloudwego/base64x v0.1.6 // indirect github.com/danieljoos/wincred v1.2.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.12 // indirect - github.com/gin-contrib/sse v1.1.0 // indirect github.com/glebarez/go-sqlite v1.21.2 // 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.30.1 // indirect - github.com/goccy/go-json v0.10.5 // indirect - github.com/goccy/go-yaml v1.19.2 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/quic-go/qpack v0.6.0 // indirect - github.com/quic-go/quic-go v0.59.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.3.1 // indirect - go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect - golang.org/x/arch v0.22.0 // indirect - golang.org/x/net v0.51.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index ea0264e..8876249 100644 --- a/go.sum +++ b/go.sum @@ -1,47 +1,18 @@ github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= -github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= -github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= -github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= -github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= -github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= -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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= -github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= -github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= -github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= -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.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= -github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= -github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -56,32 +27,14 @@ 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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk= github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -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-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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= -github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= -github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -90,37 +43,17 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -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 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 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.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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= -github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= -go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= -go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= -go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= -go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= -golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -130,10 +63,7 @@ golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= diff --git a/internal/handler/env_var_handler.go b/internal/handler/env_var_handler.go index 570d92b..d2f281e 100644 --- a/internal/handler/env_var_handler.go +++ b/internal/handler/env_var_handler.go @@ -7,8 +7,6 @@ import ( "env-manager/internal/models" "env-manager/internal/repository" - - "github.com/gin-gonic/gin" ) type EnvVarHandlerand struct { @@ -20,27 +18,36 @@ func NewEnvVarHandler(projectsRepo repository.ProjectRepository, envVarsRepo rep return &EnvVarHandlerand{projectsRepo, envVarsRepo} } -func (h *EnvVarHandlerand) GetAll(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) +func (h *EnvVarHandlerand) GetAll(w http.ResponseWriter, r *http.Request) { + pageRaw := r.URL.Query().Get("page") + if pageRaw == "" { + pageRaw = "1" + } + limitRaw := r.URL.Query().Get("limit") + if limitRaw == "" { + limitRaw = "10" + } + + page, _ := strconv.Atoi(pageRaw) + limit, _ := strconv.Atoi(limitRaw) envVars, _, err := h.envVarsRepo.FindAll(page, limit) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } envVars, err = DecryptEnvVars(&envVars) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } - c.JSON(http.StatusOK, ToResponse(true, "Env vars retrieved", envVars)) + WriteJSON(w, http.StatusOK, ToResponse(true, "Env vars retrieved", envVars)) } -func (h *EnvVarHandlerand) Create(c *gin.Context) { +func (h *EnvVarHandlerand) Create(w http.ResponseWriter, r *http.Request) { var req models.CreateEnvVarRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + if err := DecodeJSON(r, &req); err != nil { + WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } @@ -49,63 +56,63 @@ func (h *EnvVarHandlerand) Create(c *gin.Context) { // check if project exists _, err := h.projectsRepo.FindByID(uint(req.ProjectID)) if err != nil { - c.JSON(http.StatusNotFound, ToResponse(false, "project not found", nil)) + WriteJSON(w, http.StatusNotFound, ToResponse(false, "project not found", nil)) return } enc, err := EncryptValue(req.Value) if err != nil { - c.JSON(http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) + WriteJSON(w, http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) return } envVar.EncryptedVal = enc if err := h.envVarsRepo.Create(envVar); err != nil { - c.JSON(http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) + WriteJSON(w, http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) return } - c.JSON(http.StatusCreated, ToResponse(true, "Env var created", envVar)) + WriteJSON(w, http.StatusCreated, ToResponse(true, "Env var created", envVar)) } -func (h *EnvVarHandlerand) FindByID(c *gin.Context) { - id, err := strconv.ParseUint(c.Param("id"), 10, 32) +func (h *EnvVarHandlerand) FindByID(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseUint(r.PathValue("id"), 10, 32) if err != nil { - c.JSON(http.StatusBadRequest, ToResponse(false, "invalid id", nil)) + WriteJSON(w, http.StatusBadRequest, ToResponse(false, "invalid id", nil)) return } envVar, err := h.envVarsRepo.FindByID(uint(id)) if err != nil { - c.JSON(http.StatusNotFound, ToResponse(false, "env var not found", nil)) + WriteJSON(w, http.StatusNotFound, ToResponse(false, "env var not found", nil)) return } dec, err := DecryptValue(envVar.EncryptedVal) if err != nil { - c.JSON(http.StatusNotFound, ToResponse(false, err.Error(), nil)) + WriteJSON(w, http.StatusNotFound, ToResponse(false, err.Error(), nil)) return } envVar.Value = string(dec) - c.JSON(http.StatusOK, ToResponse(true, "Env var found", envVar)) + WriteJSON(w, http.StatusOK, ToResponse(true, "Env var found", envVar)) } -func (h *EnvVarHandlerand) Update(c *gin.Context) { - id, err := strconv.ParseUint(c.Param("id"), 10, 32) +func (h *EnvVarHandlerand) Update(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseUint(r.PathValue("id"), 10, 32) if err != nil { - c.JSON(http.StatusBadRequest, ToResponse(false, "invalid id", nil)) + WriteJSON(w, http.StatusBadRequest, ToResponse(false, "invalid id", nil)) return } envVar, err := h.envVarsRepo.FindByID(uint(id)) if err != nil { - c.JSON(http.StatusNotFound, ToResponse(false, "env var not found", nil)) + WriteJSON(w, http.StatusNotFound, ToResponse(false, "env var not found", nil)) return } var req models.UpdateEnvVarRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, ToResponse(false, err.Error(), nil)) + if err := DecodeJSON(r, &req); err != nil { + WriteJSON(w, http.StatusBadRequest, ToResponse(false, err.Error(), nil)) return } @@ -115,35 +122,35 @@ func (h *EnvVarHandlerand) Update(c *gin.Context) { if req.Value != "" { enc, err := EncryptValue(req.Value) if err != nil { - c.JSON(http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) + WriteJSON(w, http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) return } envVar.EncryptedVal = enc } if err := h.envVarsRepo.Update(envVar); err != nil { - c.JSON(http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) + WriteJSON(w, http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) return } - c.JSON(http.StatusOK, ToResponse(true, "Env var updated", envVar)) + WriteJSON(w, http.StatusOK, ToResponse(true, "Env var updated", envVar)) } -func (h *EnvVarHandlerand) Delete(c *gin.Context) { - id, err := strconv.ParseUint(c.Param("id"), 10, 32) +func (h *EnvVarHandlerand) Delete(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseUint(r.PathValue("id"), 10, 32) if err != nil { - c.JSON(http.StatusBadRequest, ToResponse(false, "invalid id", nil)) + WriteJSON(w, http.StatusBadRequest, ToResponse(false, "invalid id", nil)) return } _, err = h.envVarsRepo.FindByID(uint(id)) if err != nil { - c.JSON(http.StatusNotFound, ToResponse(false, fmt.Sprintf("env var with ID %v doesn't exists", id), nil)) + WriteJSON(w, http.StatusNotFound, ToResponse(false, fmt.Sprintf("env var with ID %v doesn't exists", id), nil)) return } if err := h.envVarsRepo.Delete(uint(id)); err != nil { - c.JSON(http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) + WriteJSON(w, http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) return } - c.JSON(http.StatusOK, ToResponse(true, "env var deleted", nil)) + WriteJSON(w, http.StatusOK, ToResponse(true, "env var deleted", nil)) } diff --git a/internal/handler/project_handler.go b/internal/handler/project_handler.go index 9695105..e11e4cb 100644 --- a/internal/handler/project_handler.go +++ b/internal/handler/project_handler.go @@ -7,8 +7,6 @@ import ( "env-manager/internal/models" "env-manager/internal/repository" - - "github.com/gin-gonic/gin" ) type ProjectHandler struct { @@ -19,91 +17,91 @@ func NewProjectHandler(repo repository.ProjectRepository) *ProjectHandler { return &ProjectHandler{repo} } -func (h *ProjectHandler) GetAll(c *gin.Context) { +func (h *ProjectHandler) GetAll(w http.ResponseWriter, r *http.Request) { projects, err := h.repo.FindAll() if err != nil { - c.JSON(http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) + WriteJSON(w, http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) return } - c.JSON(http.StatusOK, ToResponse(true, "Projects retrieved", projects)) + WriteJSON(w, http.StatusOK, ToResponse(true, "Projects retrieved", projects)) } -func (h *ProjectHandler) GetByID(c *gin.Context) { - id, err := strconv.ParseUint(c.Param("id"), 10, 32) +func (h *ProjectHandler) GetByID(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseUint(r.PathValue("id"), 10, 32) if err != nil { - c.JSON(http.StatusBadRequest, ToResponse(false, "invalid id", nil)) + WriteJSON(w, http.StatusBadRequest, ToResponse(false, "invalid id", nil)) return } project, err := h.repo.FindByID(uint(id)) if err != nil { - c.JSON(http.StatusNotFound, ToResponse(false, "project not found", nil)) + WriteJSON(w, http.StatusNotFound, ToResponse(false, "project not found", nil)) return } - c.JSON(http.StatusOK, ToResponse(true, "Project found", project)) + WriteJSON(w, http.StatusOK, ToResponse(true, "Project found", project)) } -func (h *ProjectHandler) GetEnvVars(c *gin.Context) { - id, err := strconv.ParseUint(c.Param("id"), 10, 32) +func (h *ProjectHandler) GetEnvVars(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseUint(r.PathValue("id"), 10, 32) if err != nil { - c.JSON(http.StatusBadRequest, ToResponse(false, "invalid id", nil)) + WriteJSON(w, http.StatusBadRequest, ToResponse(false, "invalid id", nil)) return } _, err = h.repo.FindByID(uint(id)) if err != nil { - c.JSON(http.StatusNotFound, ToResponse(false, fmt.Sprintf("project with ID %v doesn't exists", id), nil)) + WriteJSON(w, http.StatusNotFound, ToResponse(false, fmt.Sprintf("project with ID %v doesn't exists", id), nil)) return } rawEnvVars, err := h.repo.FindEnvVarsByID(uint(id)) if err != nil { - c.JSON(http.StatusNotFound, ToResponse(false, "env vars not found", nil)) + WriteJSON(w, http.StatusNotFound, ToResponse(false, "env vars not found", nil)) return } envVars, err := DecryptEnvVars(&rawEnvVars) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } - c.JSON(http.StatusOK, ToResponse(true, "Env vars found", envVars)) + WriteJSON(w, http.StatusOK, ToResponse(true, "Env vars found", envVars)) } -func (h *ProjectHandler) Create(c *gin.Context) { +func (h *ProjectHandler) Create(w http.ResponseWriter, r *http.Request) { var req models.CreateProjectRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, ToResponse(false, err.Error(), nil)) + if err := DecodeJSON(r, &req); err != nil { + WriteJSON(w, http.StatusBadRequest, ToResponse(false, err.Error(), nil)) return } project := &models.Project{Name: req.Name, Description: req.Description} if err := h.repo.Create(project); err != nil { - c.JSON(http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) + WriteJSON(w, http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) return } - c.JSON(http.StatusCreated, ToResponse(true, "Project created", project)) + WriteJSON(w, http.StatusCreated, ToResponse(true, "Project created", project)) } -func (h *ProjectHandler) Update(c *gin.Context) { - id, err := strconv.ParseUint(c.Param("id"), 10, 32) +func (h *ProjectHandler) Update(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseUint(r.PathValue("id"), 10, 32) if err != nil { - c.JSON(http.StatusBadRequest, ToResponse(false, "invalid id", nil)) + WriteJSON(w, http.StatusBadRequest, ToResponse(false, "invalid id", nil)) return } project, err := h.repo.FindByID(uint(id)) if err != nil { - c.JSON(http.StatusNotFound, ToResponse(false, "project not found", nil)) + WriteJSON(w, http.StatusNotFound, ToResponse(false, "project not found", nil)) return } var req models.UpdateProjectRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, ToResponse(false, err.Error(), nil)) + if err := DecodeJSON(r, &req); err != nil { + WriteJSON(w, http.StatusBadRequest, ToResponse(false, err.Error(), nil)) return } @@ -115,22 +113,22 @@ func (h *ProjectHandler) Update(c *gin.Context) { } if err := h.repo.Update(project); err != nil { - c.JSON(http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) + WriteJSON(w, http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) return } - c.JSON(http.StatusOK, ToResponse(true, "Project updated", project)) + WriteJSON(w, http.StatusOK, ToResponse(true, "Project updated", project)) } -func (h *ProjectHandler) Delete(c *gin.Context) { - id, err := strconv.ParseUint(c.Param("id"), 10, 32) +func (h *ProjectHandler) Delete(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseUint(r.PathValue("id"), 10, 32) if err != nil { - c.JSON(http.StatusBadRequest, ToResponse(false, "invalid id", nil)) + WriteJSON(w, http.StatusBadRequest, ToResponse(false, "invalid id", nil)) return } if err := h.repo.Delete(uint(id)); err != nil { - c.JSON(http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) + WriteJSON(w, http.StatusInternalServerError, ToResponse(false, err.Error(), nil)) return } - c.JSON(http.StatusOK, ToResponse(true, "project deleted", nil)) + WriteJSON(w, http.StatusOK, ToResponse(true, "project deleted", nil)) } diff --git a/internal/handler/utils.go b/internal/handler/utils.go index 2a1b1cc..26baeb5 100644 --- a/internal/handler/utils.go +++ b/internal/handler/utils.go @@ -1,16 +1,40 @@ package handler import ( + "encoding/json" "env-manager/internal/config" "env-manager/internal/crypto" "env-manager/internal/models" "fmt" - - "github.com/gin-gonic/gin" + "net/http" ) -func ToResponse(sucess bool, msg string, data any) gin.H { - return gin.H{"sucess": sucess, "message": msg, "data": data} +func ToResponse(sucess bool, msg string, data any) map[string]any { + return map[string]any{"sucess": sucess, "message": msg, "data": data} +} + +func WriteJSON(w http.ResponseWriter, status int, payload any) { + body, err := json.Marshal(payload) + if err != nil { + http.Error(w, "{\"error\":\"failed to encode response\"}", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _, _ = w.Write(body) +} + +func DecodeJSON(r *http.Request, dst any) error { + if r.Body == nil { + return fmt.Errorf("request body is required") + } + + if err := json.NewDecoder(r.Body).Decode(dst); err != nil { + return err + } + + return nil } func DecryptEnvVars(envVars *[]models.EnvVar) ([]models.EnvVar, error) { diff --git a/internal/router/middlewares.go b/internal/router/middlewares.go index ee29122..b04ad81 100644 --- a/internal/router/middlewares.go +++ b/internal/router/middlewares.go @@ -1,49 +1,50 @@ package router import ( + "env-manager/internal/handler" "env-manager/internal/repository" "net/http" "strings" - "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" ) -func AuthRequired(tokenRepo *repository.TokenRepository) gin.HandlerFunc { - return func(c *gin.Context) { - authHeader := c.GetHeader("Authorization") - if authHeader == "" { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) - return - } - - parts := strings.Split(authHeader, " ") - if len(parts) != 2 || parts[0] != "Bearer" { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer "}) - return - } - - tokenString := parts[1] - - if len(tokenString) < 8 { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) - return - } - - validTokens, err := (*tokenRepo).FindAllValid(tokenString[:8]) - - if err != nil { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) - return - } - for _, token := range validTokens { - if bcrypt.CompareHashAndPassword([]byte(token.HashedToken), []byte(tokenString)) == nil { - c.Next() +func AuthRequired(tokenRepo *repository.TokenRepository) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + handler.WriteJSON(w, http.StatusUnauthorized, map[string]string{"error": "Authorization header required"}) return } - } - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + parts := strings.Split(authHeader, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + handler.WriteJSON(w, http.StatusUnauthorized, map[string]string{"error": "Authorization header format must be Bearer "}) + return + } + + tokenString := parts[1] + + if len(tokenString) < 8 { + handler.WriteJSON(w, http.StatusUnauthorized, map[string]string{"error": "Invalid token"}) + return + } + + validTokens, err := (*tokenRepo).FindAllValid(tokenString[:8]) + + if err != nil { + handler.WriteJSON(w, http.StatusUnauthorized, map[string]string{"error": "Invalid token"}) + return + } + for _, token := range validTokens { + if bcrypt.CompareHashAndPassword([]byte(token.HashedToken), []byte(tokenString)) == nil { + next.ServeHTTP(w, r) + return + } + } + handler.WriteJSON(w, http.StatusUnauthorized, map[string]string{"error": "Invalid token"}) + }) } } diff --git a/internal/router/middlewares_test.go b/internal/router/middlewares_test.go index 878a1b6..f5a9c16 100644 --- a/internal/router/middlewares_test.go +++ b/internal/router/middlewares_test.go @@ -8,8 +8,6 @@ import ( "env-manager/internal/models" "env-manager/internal/repository" - - "github.com/gin-gonic/gin" ) type tokenRepoErrorMock struct{} @@ -23,19 +21,16 @@ func (tokenRepoErrorMock) FindAllValid(prefix string) ([]models.Token, error) { func (tokenRepoErrorMock) DeleteExpired() error { return nil } func TestAuthRequiredHandlesRepositoryError(t *testing.T) { - gin.SetMode(gin.TestMode) - repoIface := repository.TokenRepository(tokenRepoErrorMock{}) - r := gin.New() - r.Use(AuthRequired(&repoIface)) - r.GET("/secured", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"ok": true}) - }) + secured := AuthRequired(&repoIface)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"ok":true}`)) + })) req := httptest.NewRequest(http.MethodGet, "/secured", nil) req.Header.Set("Authorization", "Bearer abcdefgh-token") w := httptest.NewRecorder() - r.ServeHTTP(w, req) + secured.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d with body %s", w.Code, w.Body.String()) diff --git a/internal/router/router.go b/internal/router/router.go index 982f2b2..d3d12c4 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -4,43 +4,31 @@ import ( "env-manager/internal/handler" "env-manager/internal/repository" "net/http" - - "github.com/gin-gonic/gin" ) -func Setup(projectHandler *handler.ProjectHandler, envVarHandler *handler.EnvVarHandlerand, tokenRepo *repository.TokenRepository) *gin.Engine { - - r := gin.Default() - r.Use(AuthRequired(tokenRepo)) +func Setup(projectHandler *handler.ProjectHandler, envVarHandler *handler.EnvVarHandlerand, tokenRepo *repository.TokenRepository) http.Handler { + mux := http.NewServeMux() - r.GET("/health", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "ok"}) + mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { + handler.WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"}) }) - // Users routes - v1 := r.Group("/api") - { - // Users routes - projects := v1.Group("/projects") - { - projects.GET("", projectHandler.GetAll) - projects.GET("/:id", projectHandler.GetByID) - projects.GET("/:id/env-vars", projectHandler.GetEnvVars) - projects.POST("", projectHandler.Create) - projects.PUT("/:id", projectHandler.Update) - projects.DELETE("/:id", projectHandler.Delete) - } - - // Env vars routes - envVars := v1.Group("/env-vars") - { - envVars.GET("", envVarHandler.GetAll) - envVars.GET("/:id", envVarHandler.FindByID) - envVars.POST("", envVarHandler.Create) - envVars.PUT("/:id", envVarHandler.Update) - envVars.DELETE("/:id", envVarHandler.Delete) - } - } - - return r + mux.HandleFunc("GET /api/projects", projectHandler.GetAll) + mux.HandleFunc("GET /api/projects/", projectHandler.GetAll) + mux.HandleFunc("GET /api/projects/{id}", projectHandler.GetByID) + mux.HandleFunc("GET /api/projects/{id}/env-vars", projectHandler.GetEnvVars) + mux.HandleFunc("POST /api/projects", projectHandler.Create) + mux.HandleFunc("POST /api/projects/", projectHandler.Create) + mux.HandleFunc("PUT /api/projects/{id}", projectHandler.Update) + mux.HandleFunc("DELETE /api/projects/{id}", projectHandler.Delete) + + mux.HandleFunc("GET /api/env-vars", envVarHandler.GetAll) + mux.HandleFunc("GET /api/env-vars/", envVarHandler.GetAll) + mux.HandleFunc("GET /api/env-vars/{id}", envVarHandler.FindByID) + mux.HandleFunc("POST /api/env-vars", envVarHandler.Create) + mux.HandleFunc("POST /api/env-vars/", envVarHandler.Create) + mux.HandleFunc("PUT /api/env-vars/{id}", envVarHandler.Update) + mux.HandleFunc("DELETE /api/env-vars/{id}", envVarHandler.Delete) + + return AuthRequired(tokenRepo)(mux) } From 7da12014d49d54ce3644da012d29ec19bc289a7a Mon Sep 17 00:00:00 2001 From: Mosazghi Date: Thu, 23 Apr 2026 00:38:51 +0200 Subject: [PATCH 2/3] Add 'show-for' flag to token creation command for customizable display duration --- internal/server-cli/const.go | 5 ----- internal/server-cli/program_root_test.go | 4 ++++ internal/server-cli/root.go | 10 ++++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) delete mode 100644 internal/server-cli/const.go diff --git a/internal/server-cli/const.go b/internal/server-cli/const.go deleted file mode 100644 index 3494d9e..0000000 --- a/internal/server-cli/const.go +++ /dev/null @@ -1,5 +0,0 @@ -package servercli - -import "time" - -const tokenDisplayTime = 10 * time.Second diff --git a/internal/server-cli/program_root_test.go b/internal/server-cli/program_root_test.go index 02f8171..0e72ce6 100644 --- a/internal/server-cli/program_root_test.go +++ b/internal/server-cli/program_root_test.go @@ -42,6 +42,10 @@ func TestTokenCreateCommandRunE(t *testing.T) { t.Fatalf("failed to set expires-in flag: %v", err) } + if err := tokenCreateCmd.Flags().Set("show-for", "1ms"); err != nil { + t.Fatalf("failed to set show-for flag: %v", err) + } + if err := tokenCreateCmd.RunE(tokenCreateCmd, nil); err != nil { t.Fatalf("tokenCreateCmd RunE returned error: %v", err) } diff --git a/internal/server-cli/root.go b/internal/server-cli/root.go index d03c29f..a8f5aa3 100644 --- a/internal/server-cli/root.go +++ b/internal/server-cli/root.go @@ -41,9 +41,18 @@ var tokenCreateCmd = &cobra.Command{ Args: cobra.NoArgs, RunE: func(servercli *cobra.Command, args []string) error { expiresIn, err := servercli.Flags().GetString("expires-in") + if err != nil { return fmt.Errorf("invalid expires-in value: %w", err) } + tokenDisplayTimeStr, err := servercli.Flags().GetString("show-for") + if err != nil { + return fmt.Errorf("invalid show-for value: %w", err) + } + tokenDisplayTime, err := parseDuration(tokenDisplayTimeStr) + if err != nil { + return fmt.Errorf("invalid show-for duration: %w", err) + } cfg := config.Load() @@ -144,6 +153,7 @@ var serverExecCmd = &cobra.Command{ func init() { tokenCreateCmd.Flags().StringP("expires-in", "e", "1h", "Duration until the token expires (e.g. 30m, 2h, 10d)") + tokenCreateCmd.Flags().StringP("show-for", "s", "10s", "Duration to show the token after creation before hiding it (e.g. 30m, 2h)") tokenCmd.AddCommand(tokenCreateCmd) rootCmd.AddCommand(tokenCmd, serverExecCmd) } From b48733a7317b2015ff9d7bef9a6aadf5a143b8fc Mon Sep 17 00:00:00 2001 From: Mosazghi Date: Thu, 23 Apr 2026 01:39:44 +0200 Subject: [PATCH 3/3] Refactor database layer to use sqlx and goose for migrations; update models and repositories accordingly --- go.mod | 14 +-- go.sum | 32 ++++--- internal/database/migrations/00001_init.sql | 36 ++++++++ internal/database/sqlite.go | 34 +++++--- internal/database/sqlite_test.go | 25 +++++- internal/models/env_var_model.go | 16 ++-- internal/models/project_model.go | 10 +-- internal/models/token_model.go | 10 +-- internal/repository/env_var_repository.go | 95 +++++++++++++++++---- internal/repository/project_repository.go | 78 ++++++++++++++--- internal/repository/repository_test.go | 2 +- internal/repository/token_repository.go | 55 ++++++++++-- internal/server-cli/program_root_test.go | 3 +- internal/server-cli/root.go | 8 +- 14 files changed, 322 insertions(+), 96 deletions(-) create mode 100644 internal/database/migrations/00001_init.sql diff --git a/go.mod b/go.mod index 34bb7f5..d8f1e57 100644 --- a/go.mod +++ b/go.mod @@ -4,34 +4,34 @@ go 1.25.6 require ( github.com/bytedance/gopkg v0.1.3 - github.com/glebarez/sqlite v1.11.0 + github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 github.com/kardianos/service v1.2.4 + github.com/pressly/goose/v3 v3.27.0 github.com/spf13/cobra v1.10.2 github.com/zalando/go-keyring v0.2.8 golang.org/x/crypto v0.48.0 - gorm.io/gorm v1.31.1 + modernc.org/sqlite v1.48.1 ) require ( github.com/danieljoos/wincred v1.2.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect github.com/spf13/pflag v1.0.10 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.35.0 // indirect modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.48.1 // indirect ) diff --git a/go.sum b/go.sum index 8876249..23c9c12 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,6 @@ +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -7,10 +10,9 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= -github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= -github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= -github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= @@ -21,23 +23,31 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -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/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk= github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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/pressly/goose/v3 v3.27.0 h1:/D30gVTuQhu0WsNZYbJi4DMOsx1lNq+6SkLe+Wp59BM= +github.com/pressly/goose/v3 v3.27.0/go.mod h1:3ZBeCXqzkgIRvrEMDkYh1guvtoJTU5oMMuDdkutoM78= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -49,6 +59,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= @@ -59,15 +71,11 @@ golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= -gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= diff --git a/internal/database/migrations/00001_init.sql b/internal/database/migrations/00001_init.sql new file mode 100644 index 0000000..7c603f2 --- /dev/null +++ b/internal/database/migrations/00001_init.sql @@ -0,0 +1,36 @@ +-- +goose Up +CREATE TABLE IF NOT EXISTS projects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS env_vars ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_id INTEGER NOT NULL, + key TEXT NOT NULL, + encrypted_val TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(project_id) REFERENCES projects(id) +); + +CREATE INDEX IF NOT EXISTS idx_env_vars_project_id ON env_vars(project_id); + +CREATE TABLE IF NOT EXISTS tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + prefix TEXT NOT NULL, + hashed_token TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at DATETIME NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_tokens_prefix ON tokens(prefix); +CREATE INDEX IF NOT EXISTS idx_tokens_expires_at ON tokens(expires_at); + +-- +goose Down +DROP TABLE IF EXISTS env_vars; +DROP TABLE IF EXISTS tokens; +DROP TABLE IF EXISTS projects; diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go index 75eb786..221116d 100644 --- a/internal/database/sqlite.go +++ b/internal/database/sqlite.go @@ -1,29 +1,43 @@ package database import ( + "embed" "os" "path/filepath" - "env-manager/internal/models" - - "github.com/glebarez/sqlite" // ← GORM driver, pure Go, no CGO - "gorm.io/gorm" - "gorm.io/gorm/logger" + "github.com/jmoiron/sqlx" + "github.com/pressly/goose/v3" + _ "modernc.org/sqlite" ) -func NewSQLite(path string) (*gorm.DB, error) { +const migrationsDir = "migrations" + +//go:embed migrations/*.sql +var migrationFiles embed.FS + +func NewSQLite(path string) (*sqlx.DB, error) { if err := ensureDir(path); err != nil { return nil, err } - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - }) + db, err := sqlx.Open("sqlite", path) if err != nil { return nil, err } - if err := db.AutoMigrate(&models.Project{}, &models.EnvVar{}, &models.Token{}); err != nil { + if err := db.Ping(); err != nil { + _ = db.Close() + return nil, err + } + + goose.SetBaseFS(migrationFiles) + if err := goose.SetDialect("sqlite3"); err != nil { + _ = db.Close() + return nil, err + } + + if err := goose.Up(db.DB, migrationsDir); err != nil { + _ = db.Close() return nil, err } diff --git a/internal/database/sqlite_test.go b/internal/database/sqlite_test.go index 4e16fd8..3353ab9 100644 --- a/internal/database/sqlite_test.go +++ b/internal/database/sqlite_test.go @@ -1,11 +1,12 @@ package database import ( + "database/sql" "os" "path/filepath" "testing" - "env-manager/internal/models" + "github.com/jmoiron/sqlx" ) func TestEnsureDirCreatesPath(t *testing.T) { @@ -27,16 +28,17 @@ func TestNewSQLiteCreatesDatabaseAndMigrates(t *testing.T) { if err != nil { t.Fatalf("NewSQLite returned error: %v", err) } + defer db.Close() - if !db.Migrator().HasTable(&models.Project{}) { + if !hasTable(t, db, "projects") { t.Fatal("expected project table to be migrated") } - if !db.Migrator().HasTable(&models.EnvVar{}) { + if !hasTable(t, db, "env_vars") { t.Fatal("expected env_vars table to be migrated") } - if !db.Migrator().HasTable(&models.Token{}) { + if !hasTable(t, db, "tokens") { t.Fatal("expected tokens table to be migrated") } @@ -51,3 +53,18 @@ func TestNewSQLiteInvalidPath(t *testing.T) { t.Fatal("expected error for invalid database path") } } + +func hasTable(t *testing.T, db *sqlx.DB, tableName string) bool { + t.Helper() + + var name string + err := db.QueryRow(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`, tableName).Scan(&name) + if err == sql.ErrNoRows { + return false + } + if err != nil { + t.Fatalf("failed checking table existence for %s: %v", tableName, err) + } + + return name == tableName +} diff --git a/internal/models/env_var_model.go b/internal/models/env_var_model.go index cd9f812..be93624 100644 --- a/internal/models/env_var_model.go +++ b/internal/models/env_var_model.go @@ -3,14 +3,14 @@ package models import "time" type EnvVar struct { - Project Project `json:"-" gorm:"foreignKey:ProjectID"` - CreatedAt time.Time - UpdatedAt time.Time - Key string `json:"key"` - EncryptedVal string `json:"-" gorm:"column:encrypted_val"` - Value string `json:"value" gorm:"-"` - ID uint `gorm:"primarykey"` - ProjectID int `json:"project_id" gorm:"not null;index"` + Project Project `json:"-" db:"-"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + Key string `json:"key" db:"key"` + EncryptedVal string `json:"-" db:"encrypted_val"` + Value string `json:"value" db:"-"` + ID uint `db:"id"` + ProjectID int `json:"project_id" db:"project_id"` } type CreateEnvVarRequest struct { diff --git a/internal/models/project_model.go b/internal/models/project_model.go index 98418ad..f876ec8 100644 --- a/internal/models/project_model.go +++ b/internal/models/project_model.go @@ -3,11 +3,11 @@ package models import "time" type Project struct { - CreatedAt time.Time - UpdatedAt time.Time - Name string `json:"name" gorm:"unique;not null"` - Description string `json:"description" gorm:"type:text"` - ID uint `gorm:"primarykey"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + Name string `json:"name" db:"name"` + Description string `json:"description" db:"description"` + ID uint `db:"id"` } type CreateProjectRequest struct { diff --git a/internal/models/token_model.go b/internal/models/token_model.go index 47faa0a..dad3b7a 100644 --- a/internal/models/token_model.go +++ b/internal/models/token_model.go @@ -5,10 +5,10 @@ import ( ) type Token struct { - ExpiresAt time.Time `json:"expiresAt" gorm:"not null"` - CreatedAt time.Time `json:"createdAt" gorm:"not null"` - HashedToken string `json:"hashedToken" gorm:"not null"` + ExpiresAt time.Time `json:"expiresAt" db:"expires_at"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + HashedToken string `json:"hashedToken" db:"hashed_token"` // Only the 8 first chars of the token for faster lookup - Prefix string `json:"prefix"` - ID uint `json:"id" gorm:"primarykey"` + Prefix string `json:"prefix" db:"prefix"` + ID uint `json:"id" db:"id"` } diff --git a/internal/repository/env_var_repository.go b/internal/repository/env_var_repository.go index 33ff60b..1a6183c 100644 --- a/internal/repository/env_var_repository.go +++ b/internal/repository/env_var_repository.go @@ -1,9 +1,11 @@ package repository import ( + "database/sql" "env-manager/internal/models" + "time" - "gorm.io/gorm" + "github.com/jmoiron/sqlx" ) type EnvVarRepository interface { @@ -16,44 +18,107 @@ type EnvVarRepository interface { } type envVarRepository struct { - db *gorm.DB + db *sqlx.DB } -func NewEnvVarRepository(db *gorm.DB) EnvVarRepository { +func NewEnvVarRepository(db *sqlx.DB) EnvVarRepository { return &envVarRepository{db} } func (r *envVarRepository) FindAll(page, limit int) ([]models.EnvVar, int64, error) { - var envVars []models.EnvVar + if page < 1 { + page = 1 + } + if limit < 1 { + limit = 10 + } + var total int64 + if err := r.db.QueryRow(`SELECT COUNT(*) FROM env_vars`).Scan(&total); err != nil { + return nil, 0, err + } offset := (page - 1) * limit - r.db.Model(&models.EnvVar{}).Count(&total) - result := r.db.Preload("Project").Offset(offset).Limit(limit).Find(&envVars) + var envVars []models.EnvVar + err := r.db.Select(&envVars, `SELECT id, project_id, key, encrypted_val, created_at, updated_at FROM env_vars ORDER BY id LIMIT ? OFFSET ?`, limit, offset) + if err != nil { + return nil, 0, err + } - return envVars, total, result.Error + return envVars, total, nil } func (r *envVarRepository) FindByProjectID(projectID uint) ([]*models.EnvVar, error) { var envVars []*models.EnvVar - result := r.db.Where("project_id = ?", projectID).Find(&envVars) - return envVars, result.Error + err := r.db.Select(&envVars, `SELECT id, project_id, key, encrypted_val, created_at, updated_at FROM env_vars WHERE project_id = ? ORDER BY id`, projectID) + if err != nil { + return nil, err + } + return envVars, nil } - func (r *envVarRepository) FindByID(id uint) (*models.EnvVar, error) { var envVar models.EnvVar - result := r.db.First(&envVar, id) - return &envVar, result.Error + err := r.db.Get(&envVar, `SELECT id, project_id, key, encrypted_val, created_at, updated_at FROM env_vars WHERE id = ? LIMIT 1`, id) + if err != nil { + return nil, err + } + return &envVar, nil + } func (r *envVarRepository) Create(envVar *models.EnvVar) error { - return r.db.Omit("Project").Create(envVar).Error + now := time.Now().UTC() + result, err := r.db.Exec( + `INSERT INTO env_vars (project_id, key, encrypted_val, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`, + envVar.ProjectID, + envVar.Key, + envVar.EncryptedVal, + time.Now(), + time.Now(), + ) + + if err != nil { + return err + } + + id, err := result.LastInsertId() + if err != nil { + return err + } + + envVar.ID = uint(id) + envVar.CreatedAt = now.Local() + envVar.UpdatedAt = now.Local() + return nil } func (r *envVarRepository) Update(envVar *models.EnvVar) error { - return r.db.Save(envVar).Error + now := time.Now().UTC() + result, err := r.db.Exec( + `UPDATE env_vars SET project_id = ?, key = ?, encrypted_val = ?, updated_at = ? WHERE id = ?`, + envVar.ProjectID, + envVar.Key, + envVar.EncryptedVal, + time.Now(), + envVar.ID, + ) + if err != nil { + return err + } + + affected, err := result.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return sql.ErrNoRows + } + + envVar.UpdatedAt = now.Local() + return nil } func (r *envVarRepository) Delete(id uint) error { - return r.db.Delete(&models.EnvVar{}, id).Error + _, err := r.db.Exec(`DELETE FROM env_vars WHERE id = ?`, id) + return err } diff --git a/internal/repository/project_repository.go b/internal/repository/project_repository.go index 646e71d..9d7da8c 100644 --- a/internal/repository/project_repository.go +++ b/internal/repository/project_repository.go @@ -1,9 +1,11 @@ package repository import ( + "database/sql" "env-manager/internal/models" + "time" - "gorm.io/gorm" + "github.com/jmoiron/sqlx" ) type ProjectRepository interface { @@ -16,40 +18,90 @@ type ProjectRepository interface { } type projectRepository struct { - db *gorm.DB + db *sqlx.DB } -func NewProjectRepository(db *gorm.DB) ProjectRepository { +func NewProjectRepository(db *sqlx.DB) ProjectRepository { return &projectRepository{db} } func (r *projectRepository) FindAll() ([]models.Project, error) { var projects []models.Project - - result := r.db.Find(&projects) - return projects, result.Error + err := r.db.Select(&projects, `SELECT id, name, description, created_at, updated_at FROM projects ORDER BY id`) + if err != nil { + return nil, err + } + return projects, nil } func (r *projectRepository) FindByID(id uint) (*models.Project, error) { var project models.Project - result := r.db.First(&project, id) - return &project, result.Error + err := r.db.Get(&project, `SELECT id, name, description, created_at, updated_at FROM projects WHERE id = ? LIMIT 1`, id) + if err != nil { + return nil, err + } + return &project, nil } func (r *projectRepository) FindEnvVarsByID(id uint) ([]models.EnvVar, error) { var envVars []models.EnvVar - result := r.db.Where("project_id = ?", id).Find(&envVars) - return envVars, result.Error + err := r.db.Select(&envVars, `SELECT id, project_id, key, encrypted_val, created_at, updated_at FROM env_vars WHERE project_id = ? ORDER BY id`, id) + if err != nil { + return nil, err + } + return envVars, nil } func (r *projectRepository) Create(project *models.Project) error { - return r.db.Create(project).Error + now := time.Now().UTC() + result, err := r.db.Exec( + `INSERT INTO projects (name, description, created_at, updated_at) VALUES (?, ?, ?, ?)`, + project.Name, + project.Description, + time.Now(), + time.Now(), + ) + if err != nil { + return err + } + + id, err := result.LastInsertId() + if err != nil { + return err + } + + project.ID = uint(id) + project.CreatedAt = now.Local() + project.UpdatedAt = now.Local() + return nil } func (r *projectRepository) Update(project *models.Project) error { - return r.db.Save(project).Error + now := time.Now().UTC() + result, err := r.db.Exec( + `UPDATE projects SET name = ?, description = ?, updated_at = ? WHERE id = ?`, + project.Name, + project.Description, + time.Now(), + project.ID, + ) + if err != nil { + return err + } + + affected, err := result.RowsAffected() + if err != nil { + return err + } + if affected == 0 { + return sql.ErrNoRows + } + + project.UpdatedAt = now.Local() + return nil } func (r *projectRepository) Delete(id uint) error { - return r.db.Delete(&models.Project{}, id).Error + _, err := r.db.Exec(`DELETE FROM projects WHERE id = ?`, id) + return err } diff --git a/internal/repository/repository_test.go b/internal/repository/repository_test.go index ec91172..1df4a19 100644 --- a/internal/repository/repository_test.go +++ b/internal/repository/repository_test.go @@ -222,7 +222,7 @@ func TestTokenRepositoryFindAllValidAndDeleteExpired(t *testing.T) { } var count int64 - if err := db.Model(&models.Token{}).Count(&count).Error; err != nil { + if err := db.QueryRow(`SELECT COUNT(*) FROM tokens`).Scan(&count); err != nil { t.Fatalf("failed counting tokens: %v", err) } diff --git a/internal/repository/token_repository.go b/internal/repository/token_repository.go index 4f6f346..2bc9315 100644 --- a/internal/repository/token_repository.go +++ b/internal/repository/token_repository.go @@ -2,9 +2,10 @@ package repository import ( "env-manager/internal/models" + "fmt" "time" - "gorm.io/gorm" + "github.com/jmoiron/sqlx" ) type TokenRepository interface { @@ -14,23 +15,63 @@ type TokenRepository interface { } type tokenRepository struct { - db *gorm.DB + db *sqlx.DB } -func NewTokenRepository(db *gorm.DB) TokenRepository { +func NewTokenRepository(db *sqlx.DB) TokenRepository { return &tokenRepository{db} } func (r *tokenRepository) Create(token *models.Token) error { - return r.db.Create(token).Error + createdAt := token.CreatedAt + if createdAt.IsZero() { + createdAt = time.Now().UTC() + } else { + createdAt = createdAt.UTC() + } + + expiresAt := token.ExpiresAt + if expiresAt.IsZero() { + return fmt.Errorf("token expiry is required") + } + expiresAt = expiresAt.UTC() + + result, err := r.db.Exec( + `INSERT INTO tokens (prefix, hashed_token, created_at, expires_at) VALUES (?, ?, ?, ?)`, + token.Prefix, + token.HashedToken, + createdAt, + expiresAt, + ) + if err != nil { + return err + } + + id, err := result.LastInsertId() + if err != nil { + return err + } + + token.ID = uint(id) + token.CreatedAt = createdAt.Local() + token.ExpiresAt = expiresAt.Local() + return nil } func (r *tokenRepository) DeleteExpired() error { - return r.db.Where("expires_at <= ?", time.Now()).Delete(&models.Token{}).Error + _, err := r.db.Exec(`DELETE FROM tokens WHERE expires_at <= ?`, time.Now().UTC()) + return err } func (r *tokenRepository) FindAllValid(prefix string) ([]models.Token, error) { var tokens []models.Token - result := r.db.Where("prefix = ? AND expires_at > ?", prefix, time.Now()).Find(&tokens) - return tokens, result.Error + err := r.db.Select(&tokens, + `SELECT id, prefix, hashed_token, created_at, expires_at FROM tokens WHERE prefix = ? AND expires_at > ? ORDER BY id`, + prefix, + time.Now().UTC(), + ) + if err != nil { + return nil, err + } + return tokens, err } diff --git a/internal/server-cli/program_root_test.go b/internal/server-cli/program_root_test.go index 0e72ce6..f7e2d56 100644 --- a/internal/server-cli/program_root_test.go +++ b/internal/server-cli/program_root_test.go @@ -7,7 +7,6 @@ import ( "testing" "env-manager/internal/database" - "env-manager/internal/models" ) func TestProgramStartAndStop(t *testing.T) { @@ -56,7 +55,7 @@ func TestTokenCreateCommandRunE(t *testing.T) { } var count int64 - if err := db.Model(&models.Token{}).Count(&count).Error; err != nil { + if err := db.QueryRow(`SELECT COUNT(*) FROM tokens`).Scan(&count); err != nil { t.Fatalf("failed counting tokens: %v", err) } diff --git a/internal/server-cli/root.go b/internal/server-cli/root.go index a8f5aa3..ea51658 100644 --- a/internal/server-cli/root.go +++ b/internal/server-cli/root.go @@ -60,13 +60,7 @@ var tokenCreateCmd = &cobra.Command{ if err != nil { return fmt.Errorf("failed to connect to database: %w", err) } - - c, err := db.DB() - if err != nil { - return fmt.Errorf("failed to get raw DB connection: %w", err) - } - - defer c.Close() + defer db.Close() tokenRepo := repository.NewTokenRepository(db) var token models.Token