From cfb4940fe85e88e4b3d54be15d959296198e8055 Mon Sep 17 00:00:00 2001 From: optclblast Date: Sat, 28 Sep 2024 01:37:58 +0300 Subject: [PATCH] tmp --- .gitignore | 3 + README.md | 3 + cmd/main.go | 5 + docker-compose.yaml | 10 ++ go.mod | 47 ++++++++ go.sum | 154 +++++++++++++++++++++++++ internal/app/app.go | 28 +++++ internal/closer/closer.go | 40 +++++++ internal/domain/requests.go | 6 + internal/logger/builder.go | 154 +++++++++++++++++++++++++ internal/logger/discard.go | 35 ++++++ internal/logger/logger.go | 77 +++++++++++++ internal/logger/slogpretty.go | 97 ++++++++++++++++ internal/server/server.go | 15 +++ internal/storage/interface.go | 17 +++ internal/storage/models/auth.go | 12 ++ internal/storage/postgres/database.go | 89 ++++++++++++++ internal/storage/transaction.go | 66 +++++++++++ migrations/20240927205337_add_users.go | 53 +++++++++ 19 files changed, 911 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/main.go create mode 100644 docker-compose.yaml create mode 100644 go.sum create mode 100644 internal/app/app.go create mode 100644 internal/closer/closer.go create mode 100644 internal/domain/requests.go create mode 100644 internal/logger/builder.go create mode 100644 internal/logger/discard.go create mode 100644 internal/logger/logger.go create mode 100644 internal/logger/slogpretty.go create mode 100644 internal/server/server.go create mode 100644 internal/storage/interface.go create mode 100644 internal/storage/models/auth.go create mode 100644 internal/storage/postgres/database.go create mode 100644 internal/storage/transaction.go create mode 100644 migrations/20240927205337_add_users.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c114aab --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.sqlite +*.db +assets/* diff --git a/README.md b/README.md new file mode 100644 index 0000000..344c714 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# DrainCloud Light +DrainCloud Light is an all-in-one lightweight DrainCloud distribution designed to work in resource-constrained environments. +It requires **# TODO put requirements here ** diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..7905807 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..065e914 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,10 @@ +services: + database: + image: postgres:16 + container_name: draincloud-db + ports: + - 5432:5432 + environment: + POSTGRES_USERNAME: draincloud + POSTGRES_DB: draincloud + POSTGRES_PASSWORD: draincloud.dev.secret \ No newline at end of file diff --git a/go.mod b/go.mod index 8829184..024d26c 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,50 @@ module git.optclblast.xyz/draincloud/draincloud-light go 1.23.0 + +require ( + github.com/fatih/color v1.17.0 + github.com/gin-gonic/gin v1.10.0 + github.com/jackc/pgx/v5 v5.7.1 + github.com/jmoiron/sqlx v1.4.0 + github.com/mattn/go-sqlite3 v1.14.23 + github.com/pressly/goose/v3 v3.22.1 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c457cdf --- /dev/null +++ b/go.sum @@ -0,0 +1,154 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/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/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +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.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +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/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= +github.com/mattn/go-sqlite3 v1.14.23/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/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 v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pressly/goose/v3 v3.22.1 h1:2zICEfr1O3yTP9BRZMGPj7qFxQ+ik6yeo+z1LMuioLc= +github.com/pressly/goose/v3 v3.22.1/go.mod h1:xtMpbstWyCpyH+0cxLTMCENWBG+0CSxvTsXhW95d5eo= +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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/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= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/sqlite v1.33.0 h1:WWkA/T2G17okiLGgKAj4/RMIvgyMT19yQ038160IeYk= +modernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..e0891c0 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,28 @@ +package app + +import ( + "github.com/gin-gonic/gin" +) + +type DrainCloud struct { + mux *gin.Engine +} + +func New() *DrainCloud { + mux := gin.Default() + + d := new(DrainCloud) + + authGroup := mux.Group("/auth") + { + authGroup.POST("/register", d.Register) + } + + d.mux = mux + + return d +} + +func (d *DrainCloud) Register(ctx *gin.Context) { + +} diff --git a/internal/closer/closer.go b/internal/closer/closer.go new file mode 100644 index 0000000..63f59ed --- /dev/null +++ b/internal/closer/closer.go @@ -0,0 +1,40 @@ +package closer + +import ( + "context" + "errors" + + "git.optclblast.xyz/draincloud/draincloud-light/internal/logger" +) + +var globalCloser *Closer = &Closer{ + closeFns: make([]func() error, 0), +} + +type Closer struct { + closeFns []func() error +} + +func (c *Closer) Add(fn func() error) { + c.closeFns = append(c.closeFns, fn) +} + +func (c *Closer) Close() error { + var commonErr error + for _, fn := range c.closeFns { + if err := fn(); err != nil { + logger.Error(context.Background(), "[closer][Close] error at close func call", logger.Err(err)) + commonErr = errors.Join(commonErr, err) + } + } + + return commonErr +} + +func Add(fn func() error) { + globalCloser.Add(fn) +} + +func Close() error { + return globalCloser.Close() +} diff --git a/internal/domain/requests.go b/internal/domain/requests.go new file mode 100644 index 0000000..db87e3a --- /dev/null +++ b/internal/domain/requests.go @@ -0,0 +1,6 @@ +package domain + +type RegisterRequest struct { + Login string `json:"login"` + Password string `json:"password"` +} diff --git a/internal/logger/builder.go b/internal/logger/builder.go new file mode 100644 index 0000000..e93ebca --- /dev/null +++ b/internal/logger/builder.go @@ -0,0 +1,154 @@ +package logger + +import ( + "context" + "io" + "log/slog" + "os" + "strings" +) + +type _key string + +//nolint:gochecknoglobals // ... +var loggerKey _key = "_core_logger" + +type LoggerOpt func(p *loggerParams) + +func NewLoggerContext(ctx context.Context, opts ...LoggerOpt) context.Context { + p := new(loggerParams) + + for _, o := range opts { + o(p) + } + + log := p.build() + + return context.WithValue(ctx, loggerKey, log) +} + +type loggerParams struct { + local bool + addSource bool + lvl slog.Level + writers []io.Writer +} + +func WithWriter(w io.Writer) LoggerOpt { + return func(p *loggerParams) { + p.writers = append(p.writers, w) + } +} + +func WithLevel(l slog.Level) LoggerOpt { + return func(p *loggerParams) { + p.lvl = l + } +} + +func Local() LoggerOpt { + return func(p *loggerParams) { + p.local = true + } +} + +func WithSource() LoggerOpt { + return func(p *loggerParams) { + p.addSource = true + } +} + +func Err(err error) slog.Attr { + return slog.Attr{ + Key: "error", + Value: slog.StringValue(err.Error()), + } +} + +func MapLevel(lvl string) slog.Level { + switch strings.ToLower(lvl) { + case "debug": + return LevelDebug + case "info": + return LevelInfo + case "notice": + return LevelNotice + case "warn": + return LevelWarn + case "error": + return LevelError + case "critical": + return LevelCritial + case "alert": + return LevelAlert + case "emergency": + return LevelEmergency + default: + return LevelInfo + } +} + +func (b *loggerParams) build() *slog.Logger { + if len(b.writers) == 0 { + b.writers = append(b.writers, os.Stdout) + } + + w := io.MultiWriter(b.writers...) + + if b.local { + opts := prettyHandlerOptions{ + SlogOpts: &slog.HandlerOptions{ + Level: b.lvl, + AddSource: b.addSource, + }, + } + + handler := opts.newPrettyHandler(w) + + return slog.New(handler) + } + + return newLogger(b.lvl, w) +} + +func newLogger(lvl slog.Level, w io.Writer) *slog.Logger { + return slog.New( + slog.NewJSONHandler(w, &slog.HandlerOptions{ + Level: lvl, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.LevelKey { + level := a.Value.Any().(slog.Level) + + switch { + case level < LevelInfo: + a.Value = slog.StringValue("DEBUG") + case level < LevelNotice: + a.Value = slog.StringValue("INFO") + case level < LevelWarn: + a.Value = slog.StringValue("NOTICE") + case level < LevelError: + a.Value = slog.StringValue("WARNING") + case level < LevelCritial: + a.Value = slog.StringValue("ERROR") + case level < LevelAlert: + a.Value = slog.StringValue("CRITICAL") + case level < LevelEmergency: + a.Value = slog.StringValue("ALERT") + default: + a.Value = slog.StringValue("EMERGENCY") + } + } + + return a + }, + }), + ) +} + +func loggerFromCtx(ctx context.Context) *slog.Logger { + if l, ok := ctx.Value(loggerKey).(*slog.Logger); ok { + return l + } + + return globalLogger +} diff --git a/internal/logger/discard.go b/internal/logger/discard.go new file mode 100644 index 0000000..a9f6a56 --- /dev/null +++ b/internal/logger/discard.go @@ -0,0 +1,35 @@ +package logger + +import ( + "context" + "log/slog" +) + +//nolint:unused //... +func newDiscardLogger() *slog.Logger { + return slog.New(newDiscardHandler()) +} + +//nolint:unused //... +type DiscardHandler struct{} + +//nolint:unused //... +func newDiscardHandler() *DiscardHandler { + return &DiscardHandler{} +} + +func (h *DiscardHandler) Handle(_ context.Context, _ slog.Record) error { + return nil +} + +func (h *DiscardHandler) WithAttrs(_ []slog.Attr) slog.Handler { + return h +} + +func (h *DiscardHandler) WithGroup(_ string) slog.Handler { + return h +} + +func (h *DiscardHandler) Enabled(_ context.Context, _ slog.Level) bool { + return false +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..e561d0c --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,77 @@ +package logger + +import ( + "context" + "log/slog" + "os" +) + +//nolint:gochecknoglobals // ... +var globalLogger *slog.Logger = newLogger(LevelInfo, os.Stdout) + +const ( + LevelEmergency = slog.Level(10000) + LevelAlert = slog.Level(1000) + LevelCritial = slog.Level(100) + LevelError = slog.LevelError + LevelWarn = slog.LevelWarn + LevelNotice = slog.Level(2) + LevelInfo = slog.LevelInfo + LevelDebug = slog.LevelDebug +) + +func Fatal(ctx context.Context, message string, attrs ...any) { + l := loggerFromCtx(ctx) + + l.Log(ctx, LevelEmergency, message, attrs...) + + os.Exit(1) +} + +func Emergency(ctx context.Context, message string, attrs ...any) { + l := loggerFromCtx(ctx) + + l.Log(ctx, LevelEmergency, message, attrs...) +} + +func Alert(ctx context.Context, message string, attrs ...any) { + l := loggerFromCtx(ctx) + + l.Log(ctx, LevelAlert, message, attrs...) +} + +func Critial(ctx context.Context, message string, attrs ...any) { + l := loggerFromCtx(ctx) + + l.Log(ctx, LevelCritial, message, attrs...) +} + +func Error(ctx context.Context, message string, attrs ...any) { + l := loggerFromCtx(ctx) + + l.ErrorContext(ctx, message, attrs...) +} + +func Warn(ctx context.Context, message string, attrs ...any) { + l := loggerFromCtx(ctx) + + l.WarnContext(ctx, message, attrs...) +} + +func Notice(ctx context.Context, message string, attrs ...any) { + l := loggerFromCtx(ctx) + + l.Log(ctx, LevelNotice, message, attrs...) +} + +func Info(ctx context.Context, message string, attrs ...any) { + l := loggerFromCtx(ctx) + + l.InfoContext(ctx, message, attrs...) +} + +func Debug(ctx context.Context, message string, attrs ...any) { + l := loggerFromCtx(ctx) + + l.Log(ctx, LevelCritial, message, attrs...) +} diff --git a/internal/logger/slogpretty.go b/internal/logger/slogpretty.go new file mode 100644 index 0000000..0bc3f9e --- /dev/null +++ b/internal/logger/slogpretty.go @@ -0,0 +1,97 @@ +package logger + +import ( + "context" + "encoding/json" + "io" + stdlog "log" + "log/slog" + + "github.com/fatih/color" +) + +type prettyHandlerOptions struct { + SlogOpts *slog.HandlerOptions +} + +type prettyHandler struct { + opts prettyHandlerOptions + slog.Handler + l *stdlog.Logger + attrs []slog.Attr +} + +func (opts prettyHandlerOptions) newPrettyHandler( + out io.Writer, +) *prettyHandler { + h := &prettyHandler{ + Handler: slog.NewJSONHandler(out, opts.SlogOpts), + l: stdlog.New(out, "", 0), + } + + return h +} + +func (h *prettyHandler) Handle(_ context.Context, r slog.Record) error { + level := r.Level.String() + ":" + + switch r.Level { + case slog.LevelDebug: + level = color.MagentaString(level) + case slog.LevelInfo: + level = color.BlueString(level) + case slog.LevelWarn: + level = color.YellowString(level) + case slog.LevelError: + level = color.RedString(level) + } + + fields := make(map[string]interface{}, r.NumAttrs()) + + r.Attrs(func(a slog.Attr) bool { + fields[a.Key] = a.Value.Any() + + return true + }) + + for _, a := range h.attrs { + fields[a.Key] = a.Value.Any() + } + + var b []byte + var err error + + if len(fields) > 0 { + b, err = json.MarshalIndent(fields, "", " ") + if err != nil { + return err + } + } + + timeStr := r.Time.Format("[15:05:05.000]") + msg := color.CyanString(r.Message) + + h.l.Println( + timeStr, + level, + msg, + color.WhiteString(string(b)), + ) + + return nil +} + +func (h *prettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &prettyHandler{ + Handler: h.Handler, + l: h.l, + attrs: attrs, + } +} + +func (h *prettyHandler) WithGroup(name string) slog.Handler { + return &prettyHandler{ + Handler: h.Handler.WithGroup(name), + l: h.l, + } +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..45f3c05 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,15 @@ +package server + +import ( + "context" + "net/http" +) + +type Server struct { + mux *http.ServeMux // Mux + ctx context.Context // Global application context +} + +func RegisterHandler(ctx context.Context, h http.HandlerFunc) { + +} diff --git a/internal/storage/interface.go b/internal/storage/interface.go new file mode 100644 index 0000000..e3e8062 --- /dev/null +++ b/internal/storage/interface.go @@ -0,0 +1,17 @@ +package storage + +import ( + "context" + + "git.optclblast.xyz/draincloud/draincloud-light/internal/storage/models" +) + +type Database interface { + AuthStorage +} + +type AuthStorage interface { + AddUser(ctx context.Context, user *models.User) error + GetUserByLogin(ctx context.Context, login string) (*models.User, error) + GetUserByID(ctx context.Context, id uint64) (*models.User, error) +} diff --git a/internal/storage/models/auth.go b/internal/storage/models/auth.go new file mode 100644 index 0000000..ae5b230 --- /dev/null +++ b/internal/storage/models/auth.go @@ -0,0 +1,12 @@ +package models + +import "time" + +type User struct { + ID uint64 + Username string + Login string + PasswordHash []byte + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/storage/postgres/database.go b/internal/storage/postgres/database.go new file mode 100644 index 0000000..3f473be --- /dev/null +++ b/internal/storage/postgres/database.go @@ -0,0 +1,89 @@ +package postgres + +import ( + "context" + "fmt" + "time" + + "git.optclblast.xyz/draincloud/draincloud-light/internal/closer" + "git.optclblast.xyz/draincloud/draincloud-light/internal/logger" + "git.optclblast.xyz/draincloud/draincloud-light/internal/storage/models" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type Database struct { + db *pgx.Conn +} + +func New(ctx context.Context, dsn string) *Database { + db, err := pgx.Connect(ctx, dsn) + if err != nil { + logger.Fatal(ctx, "failed to connect to postgres", logger.Err(err)) + } + + closer.Add(func() error { + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + return db.Close(ctx) + }) + + return &Database{db: db} +} + +type dbtx interface { + Exec(ctx context.Context, stmt string, args ...any) (pgconn.CommandTag, error) + QueryRow(ctx context.Context, sql string, args ...any) pgx.Row + Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) +} + +func (d *Database) AddUser(ctx context.Context, login string, username string, passwordHash []byte) (uint64, error) { + return addUser(ctx, d.db, login, username, passwordHash) +} + +func (d *Database) GetUserByID(ctx context.Context, id uint64) (*models.User, error) { + return getUserByID(ctx, d.db, id) +} + +func (d *Database) GetUserByLogin(ctx context.Context, login string) (*models.User, error) { + return getUserByLogin(ctx, d.db, login) +} + +func addUser(ctx context.Context, conn dbtx, login string, username string, passwordHash []byte) (uint64, error) { + const stmt = `INSERT INTO users (login,username,password) + VALUES ($1,$2,$3,$4) RETURNING id` + + row := conn.QueryRow(ctx, stmt, login, username, passwordHash) + + var id uint64 + + if err := row.Scan(&id); err != nil { + return 0, fmt.Errorf("failed to insert user data into users table: %w", err) + } + + return id, nil +} + +func getUserByID(ctx context.Context, conn dbtx, id uint64) (*models.User, error) { + const stmt = `SELECT * FROM users WHERE id = $1 LIMIT 1` + u := new(models.User) + + row := conn.QueryRow(ctx, stmt, id) + if err := row.Scan(&u.ID, &u.Login, &u.Username, &u.PasswordHash, &u.CreatedAt, &u.UpdatedAt); err != nil { + return nil, fmt.Errorf("failed to fetch user by id: %w", err) + } + + return u, nil +} + +func getUserByLogin(ctx context.Context, conn dbtx, login string) (*models.User, error) { + const stmt = `SELECT * FROM users WHERE login = $1 LIMIT 1` + u := new(models.User) + + row := conn.QueryRow(ctx, stmt, login) + if err := row.Scan(&u.ID, &u.Login, &u.Username, &u.PasswordHash, &u.CreatedAt, &u.UpdatedAt); err != nil { + return nil, fmt.Errorf("failed to fetch user by login: %w", err) + } + + return u, nil +} diff --git a/internal/storage/transaction.go b/internal/storage/transaction.go new file mode 100644 index 0000000..24a2661 --- /dev/null +++ b/internal/storage/transaction.go @@ -0,0 +1,66 @@ +package storage + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "github.com/jmoiron/sqlx" +) + +type txKey struct{} + +var ctxKey txKey = txKey{} + +type DBTX interface { + sqlx.Ext + sqlx.ExtContext +} + +func Transaction(ctx context.Context, db *sqlx.DB, fn func(context.Context) error) (err error) { + tx := txFromContext(ctx) + if tx == nil { + tx, err = db.BeginTxx(ctx, &sql.TxOptions{ + Isolation: sql.LevelRepeatableRead, + }) + if err != nil { + return fmt.Errorf("failed to begin tx: %w", err) + } + + defer func() { + if err == nil { + err = tx.Commit() + } + if err != nil { + if rbErr := tx.Rollback(); rbErr != nil { + err = errors.Join(err, rbErr) + } + } + }() + + ctx = txContext(ctx, tx) + } + + return fn(ctx) +} + +func Conn(ctx context.Context, db DBTX) DBTX { + if tx := txFromContext(ctx); tx != nil { + return tx + } + + return db +} + +func txFromContext(ctx context.Context) *sqlx.Tx { + if tx, ok := ctx.Value(ctxKey).(*sqlx.Tx); ok { + return tx + } + + return nil +} + +func txContext(parent context.Context, tx *sqlx.Tx) context.Context { + return context.WithValue(parent, tx, ctxKey) +} diff --git a/migrations/20240927205337_add_users.go b/migrations/20240927205337_add_users.go new file mode 100644 index 0000000..e769af3 --- /dev/null +++ b/migrations/20240927205337_add_users.go @@ -0,0 +1,53 @@ +package migrations + +import ( + "context" + "database/sql" + + "git.optclblast.xyz/draincloud/draincloud-light/internal/logger" + "github.com/pressly/goose/v3" +) + +func init() { + goose.AddMigrationContext(upAddUsers, downAddUsers) +} + +func upAddUsers(ctx context.Context, tx *sql.Tx) error { + const stmt = ` + CREATE TABLE IF NOT EXISTS users ( + id bigserial PRIMARY KEY + , username TEXT DEFAULT NULL + , login TEXT NOT NULL UNIQUE + , PASSWORD bytea NOT NULL + , created_at timestamptz DEFAULT current_timestamp + , updated_at timestamptz DEFAULT current_timestamp + ); + + ALTER TABLE users ADD CONSTRAINT users_username_len CHECK(length(username) > 250) NOT VALID; + ALTER TABLE users ADD CONSTRAINT users_login_len CHECK(length(username) > 250) NOT VALID; + + CREATE INDEX CONCURRENTLY idx_users_login ON + users (login); + CREATE INDEX CONCURRENTLY idx_users_username ON + users (username);` + + if _, err := tx.ExecContext(ctx, stmt); err != nil { + logger.Error(ctx, "[migration] error", logger.Err(err)) + return err + } + + return nil +} + +func downAddUsers(ctx context.Context, tx *sql.Tx) error { + const stmt = ` + DROP INDEX CONCURRENTLY IF EXISTS idx_users_login; + DROP INDEX CONCURRENTLY IF EXISTS idx_users_username; + DROP TABLE IF EXISTS users;` + + if _, err := tx.ExecContext(ctx, stmt); err != nil { + logger.Error(ctx, "[migration] error", logger.Err(err)) + return err + } + return nil +}