implement graphQL and init postgres

This commit is contained in:
Fran Jurmanović
2025-07-06 19:19:36 +02:00
parent 016728532c
commit 26a0d33592
25 changed files with 1713 additions and 314 deletions

13
go.mod
View File

@@ -9,7 +9,7 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
go.uber.org/dig v1.17.1 go.uber.org/dig v1.17.1
golang.org/x/crypto v0.39.0 golang.org/x/crypto v0.39.0
gorm.io/driver/sqlite v1.5.6 gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.11 gorm.io/gorm v1.25.11
) )
@@ -20,23 +20,28 @@ require (
github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/swaggo/swag v1.16.3 // indirect github.com/swaggo/swag v1.16.3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.33.0 // indirect golang.org/x/tools v0.34.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

33
go.sum
View File

@@ -2,6 +2,7 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
@@ -20,6 +21,14 @@ github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXe
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 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 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -34,23 +43,23 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 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-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
@@ -69,20 +78,20 @@ golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

View File

@@ -2,6 +2,9 @@ package api
import ( import (
"omega-server/local/controller" "omega-server/local/controller"
"omega-server/local/graphql/handler"
graphqlService "omega-server/local/graphql/service"
"omega-server/local/service"
"omega-server/local/utl/common" "omega-server/local/utl/common"
"omega-server/local/utl/configs" "omega-server/local/utl/configs"
"omega-server/local/utl/logging" "omega-server/local/utl/logging"
@@ -33,5 +36,35 @@ func Init(di *dig.Container, app *fiber.App) {
logging.Panic("unable to bind routes") logging.Panic("unable to bind routes")
} }
// Initialize GraphQL
initGraphQL(di, groups)
controller.InitializeControllers(di) controller.InitializeControllers(di)
} }
// initGraphQL initializes GraphQL endpoints
func initGraphQL(di *dig.Container, groups fiber.Router) {
err := di.Invoke(func(membershipService *service.MembershipService) error {
// Create GraphQL service
gqlService := graphqlService.NewGraphQLService(membershipService)
_ = gqlService // Use the service (placeholder)
// Create GraphQL handler
graphqlHandler := handler.NewGraphQLHandler(membershipService)
// Register GraphQL routes
groups.Post("/graphql", graphqlHandler.Handle)
// GraphQL playground/schema endpoint
groups.Get("/graphql", func(c *fiber.Ctx) error {
return c.SendString(graphqlHandler.GetSchema())
})
logging.Info("GraphQL endpoint initialized at /graphql")
return nil
})
if err != nil {
logging.Panic("failed to initialize GraphQL: " + err.Error())
}
}

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"omega-server/local/middleware" "omega-server/local/middleware"
"omega-server/local/model"
"omega-server/local/service" "omega-server/local/service"
"omega-server/local/utl/common" "omega-server/local/utl/common"
"omega-server/local/utl/error_handler" "omega-server/local/utl/error_handler"
@@ -52,7 +53,7 @@ func NewMembershipController(service *service.MembershipService, auth *middlewar
// Login handles user login. // Login handles user login.
func (c *MembershipController) Login(ctx *fiber.Ctx) error { func (c *MembershipController) Login(ctx *fiber.Ctx) error {
type request struct { type request struct {
Username string `json:"username"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
} }
@@ -62,7 +63,7 @@ func (c *MembershipController) Login(ctx *fiber.Ctx) error {
} }
logging.Debug("Login request received") logging.Debug("Login request received")
token, err := c.service.Login(ctx.UserContext(), req.Username, req.Password) token, err := c.service.Login(ctx.UserContext(), req.Email, req.Password)
if err != nil { if err != nil {
return c.errorHandler.HandleAuthError(ctx, err) return c.errorHandler.HandleAuthError(ctx, err)
} }
@@ -72,23 +73,35 @@ func (c *MembershipController) Login(ctx *fiber.Ctx) error {
// CreateUser creates a new user. // CreateUser creates a new user.
func (mc *MembershipController) CreateUser(c *fiber.Ctx) error { func (mc *MembershipController) CreateUser(c *fiber.Ctx) error {
type request struct { var req model.UserCreateRequest
Username string `json:"username"`
Password string `json:"password"`
Role string `json:"role"`
}
var req request
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return mc.errorHandler.HandleParsingError(c, err) return mc.errorHandler.HandleParsingError(c, err)
} }
user, err := mc.service.CreateUser(c.UserContext(), req.Username, req.Password, req.Role) // Validate request
if err := req.Validate(); err != nil {
return mc.errorHandler.HandleValidationError(c, err, "user_create_request")
}
// Map to domain model
user, err := req.ToUser()
if err != nil {
return mc.errorHandler.HandleValidationError(c, err, "user_mapping")
}
// Extract role names from request
roleIDs := req.RoleIDs
if len(roleIDs) == 0 {
roleIDs = []string{"user"} // default role
}
// Call service with domain model
createdUser, err := mc.service.CreateUser(c.UserContext(), user, roleIDs)
if err != nil { if err != nil {
return mc.errorHandler.HandleServiceError(c, err) return mc.errorHandler.HandleServiceError(c, err)
} }
return c.JSON(user) return c.JSON(createdUser.ToResponse())
} }
// ListUsers lists all users. // ListUsers lists all users.
@@ -98,7 +111,13 @@ func (mc *MembershipController) ListUsers(c *fiber.Ctx) error {
return mc.errorHandler.HandleServiceError(c, err) return mc.errorHandler.HandleServiceError(c, err)
} }
return c.JSON(users) // Convert to response format
userResponses := make([]*model.UserResponse, len(users))
for i, user := range users {
userResponses[i] = user.ToResponse()
}
return c.JSON(userResponses)
} }
// GetUser gets a single user by ID. // GetUser gets a single user by ID.
@@ -113,7 +132,7 @@ func (mc *MembershipController) GetUser(c *fiber.Ctx) error {
return mc.errorHandler.HandleNotFoundError(c, "User") return mc.errorHandler.HandleNotFoundError(c, "User")
} }
return c.JSON(user) return c.JSON(user.ToResponse())
} }
// GetMe returns the currently authenticated user's details. // GetMe returns the currently authenticated user's details.
@@ -128,10 +147,7 @@ func (mc *MembershipController) GetMe(c *fiber.Ctx) error {
return mc.errorHandler.HandleNotFoundError(c, "User") return mc.errorHandler.HandleNotFoundError(c, "User")
} }
// Sanitize the user object to not expose password return c.JSON(user.ToResponse())
user.PasswordHash = ""
return c.JSON(user)
} }
// DeleteUser deletes a user. // DeleteUser deletes a user.
@@ -156,17 +172,22 @@ func (mc *MembershipController) UpdateUser(c *fiber.Ctx) error {
return mc.errorHandler.HandleUUIDError(c, "user ID") return mc.errorHandler.HandleUUIDError(c, "user ID")
} }
var req service.UpdateUserRequest var req model.UserUpdateRequest
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return mc.errorHandler.HandleParsingError(c, err) return mc.errorHandler.HandleParsingError(c, err)
} }
user, err := mc.service.UpdateUser(c.UserContext(), id, req) // Validate request
if err := req.Validate(); err != nil {
return mc.errorHandler.HandleValidationError(c, err, "user_update_request")
}
user, err := mc.service.UpdateUser(c.UserContext(), id, &req)
if err != nil { if err != nil {
return mc.errorHandler.HandleServiceError(c, err) return mc.errorHandler.HandleServiceError(c, err)
} }
return c.JSON(user) return c.JSON(user.ToResponse())
} }
// GetRoles returns all available roles. // GetRoles returns all available roles.

View File

@@ -0,0 +1,421 @@
package handler
import (
"context"
"omega-server/local/model"
"omega-server/local/service"
"strings"
"github.com/gofiber/fiber/v2"
)
// GraphQLHandler handles GraphQL requests
type GraphQLHandler struct {
membershipService *service.MembershipService
}
// NewGraphQLHandler creates a new GraphQL handler
func NewGraphQLHandler(membershipService *service.MembershipService) *GraphQLHandler {
return &GraphQLHandler{
membershipService: membershipService,
}
}
// GraphQLRequest represents a GraphQL request
type GraphQLRequest struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables"`
}
// GraphQLResponse represents a GraphQL response
type GraphQLResponse struct {
Data interface{} `json:"data,omitempty"`
Errors []GraphQLError `json:"errors,omitempty"`
}
// GraphQLError represents a GraphQL error
type GraphQLError struct {
Message string `json:"message"`
Path []string `json:"path,omitempty"`
}
// Handle processes GraphQL requests
func (h *GraphQLHandler) Handle(c *fiber.Ctx) error {
var req GraphQLRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(GraphQLResponse{
Errors: []GraphQLError{{Message: "Invalid request body"}},
})
}
// Basic query parsing and handling
ctx := c.UserContext()
// Simple query routing based on query string
query := strings.TrimSpace(req.Query)
switch {
case strings.Contains(query, "mutation") && strings.Contains(query, "login"):
return h.handleLogin(c, ctx, req)
case strings.Contains(query, "mutation") && strings.Contains(query, "createUser"):
return h.handleCreateUser(c, ctx, req)
case strings.Contains(query, "mutation") && strings.Contains(query, "createProject"):
return h.handleCreateProject(c, ctx, req)
case strings.Contains(query, "mutation") && strings.Contains(query, "createTask"):
return h.handleCreateTask(c, ctx, req)
case strings.Contains(query, "query") && strings.Contains(query, "me"):
return h.handleMe(c, ctx, req)
case strings.Contains(query, "query") && strings.Contains(query, "users"):
return h.handleUsers(c, ctx, req)
case strings.Contains(query, "query") && strings.Contains(query, "projects"):
return h.handleProjects(c, ctx, req)
case strings.Contains(query, "query") && strings.Contains(query, "tasks"):
return h.handleTasks(c, ctx, req)
default:
return c.Status(400).JSON(GraphQLResponse{
Errors: []GraphQLError{{Message: "Query not supported"}},
})
}
}
// handleLogin handles login mutations
func (h *GraphQLHandler) handleLogin(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
// Extract variables
email, ok := req.Variables["email"].(string)
if !ok {
return c.Status(400).JSON(GraphQLResponse{
Errors: []GraphQLError{{Message: "Email is required"}},
})
}
password, ok := req.Variables["password"].(string)
if !ok {
return c.Status(400).JSON(GraphQLResponse{
Errors: []GraphQLError{{Message: "Password is required"}},
})
}
// Call service
token, err := h.membershipService.Login(ctx, email, password)
if err != nil {
return c.Status(401).JSON(GraphQLResponse{
Errors: []GraphQLError{{Message: "Invalid credentials"}},
})
}
// Mock user data for now
userData := map[string]interface{}{
"id": "1",
"email": email,
"fullName": "User",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
}
response := map[string]interface{}{
"login": map[string]interface{}{
"token": token,
"user": userData,
},
}
return c.JSON(GraphQLResponse{Data: response})
}
// handleCreateUser handles user creation mutations
func (h *GraphQLHandler) handleCreateUser(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
// Extract variables
email, ok := req.Variables["email"].(string)
if !ok {
return c.Status(400).JSON(GraphQLResponse{
Errors: []GraphQLError{{Message: "Email is required"}},
})
}
password, ok := req.Variables["password"].(string)
if !ok {
return c.Status(400).JSON(GraphQLResponse{
Errors: []GraphQLError{{Message: "Password is required"}},
})
}
fullName, ok := req.Variables["fullName"].(string)
if !ok {
fullName = ""
}
// Create domain model
user := &model.User{
Email: email,
FullName: fullName,
}
if err := user.SetPassword(password); err != nil {
return c.Status(400).JSON(GraphQLResponse{
Errors: []GraphQLError{{Message: err.Error()}},
})
}
// Call service
createdUser, err := h.membershipService.CreateUser(ctx, user, []string{"user"})
if err != nil {
return c.Status(400).JSON(GraphQLResponse{
Errors: []GraphQLError{{Message: err.Error()}},
})
}
// Convert user to response format
userData := map[string]interface{}{
"id": createdUser.ID,
"email": createdUser.Email,
"fullName": createdUser.FullName,
"createdAt": createdUser.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
"updatedAt": createdUser.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
response := map[string]interface{}{
"createUser": userData,
}
return c.JSON(GraphQLResponse{Data: response})
}
// handleMe handles me queries
func (h *GraphQLHandler) handleMe(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
// This would typically extract user ID from JWT token
// For now, return mock data
userData := map[string]interface{}{
"id": "1",
"email": "admin@example.com",
"fullName": "System Administrator",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
}
response := map[string]interface{}{
"me": userData,
}
return c.JSON(GraphQLResponse{Data: response})
}
// handleUsers handles users queries
func (h *GraphQLHandler) handleUsers(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
// Call service
users, err := h.membershipService.ListUsers(ctx)
if err != nil {
return c.Status(500).JSON(GraphQLResponse{
Errors: []GraphQLError{{Message: "Failed to fetch users"}},
})
}
// Convert users to response format
usersData := make([]map[string]interface{}, len(users))
for i, user := range users {
usersData[i] = map[string]interface{}{
"id": user.ID,
"email": user.Email,
"fullName": user.FullName,
"createdAt": user.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
"updatedAt": user.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}
response := map[string]interface{}{
"users": usersData,
}
return c.JSON(GraphQLResponse{Data: response})
}
// handleCreateProject handles project creation mutations
func (h *GraphQLHandler) handleCreateProject(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
// Extract variables
name, ok := req.Variables["name"].(string)
if !ok {
return c.Status(400).JSON(GraphQLResponse{
Errors: []GraphQLError{{Message: "Name is required"}},
})
}
ownerId, ok := req.Variables["ownerId"].(string)
if !ok {
return c.Status(400).JSON(GraphQLResponse{
Errors: []GraphQLError{{Message: "Owner ID is required"}},
})
}
description, _ := req.Variables["description"].(string)
// Mock project data for now
projectData := map[string]interface{}{
"id": "mock-project-id",
"name": name,
"description": description,
"ownerId": ownerId,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
}
response := map[string]interface{}{
"createProject": projectData,
}
return c.JSON(GraphQLResponse{Data: response})
}
// handleCreateTask handles task creation mutations
func (h *GraphQLHandler) handleCreateTask(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
// Extract variables
title, ok := req.Variables["title"].(string)
if !ok {
return c.Status(400).JSON(GraphQLResponse{
Errors: []GraphQLError{{Message: "Title is required"}},
})
}
projectId, ok := req.Variables["projectId"].(string)
if !ok {
return c.Status(400).JSON(GraphQLResponse{
Errors: []GraphQLError{{Message: "Project ID is required"}},
})
}
description, _ := req.Variables["description"].(string)
status, _ := req.Variables["status"].(string)
if status == "" {
status = "todo"
}
priority, _ := req.Variables["priority"].(string)
if priority == "" {
priority = "medium"
}
// Mock task data for now
taskData := map[string]interface{}{
"id": "mock-task-id",
"title": title,
"description": description,
"status": status,
"priority": priority,
"projectId": projectId,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
}
response := map[string]interface{}{
"createTask": taskData,
}
return c.JSON(GraphQLResponse{Data: response})
}
// handleProjects handles projects queries
func (h *GraphQLHandler) handleProjects(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
// Mock empty projects list for now
response := map[string]interface{}{
"projects": []interface{}{},
}
return c.JSON(GraphQLResponse{Data: response})
}
// handleTasks handles tasks queries
func (h *GraphQLHandler) handleTasks(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
// Mock empty tasks list for now
response := map[string]interface{}{
"tasks": []interface{}{},
}
return c.JSON(GraphQLResponse{Data: response})
}
// GetSchema returns the GraphQL schema
func (h *GraphQLHandler) GetSchema() string {
return `
# Core Types
type User {
id: String!
email: String!
fullName: String
createdAt: String!
updatedAt: String!
}
type Project {
id: String!
name: String!
description: String
ownerId: String!
createdAt: String!
updatedAt: String!
}
type Task {
id: String!
title: String!
description: String
status: String!
priority: String!
projectId: String!
createdAt: String!
updatedAt: String!
}
# Input Types
input LoginInput {
email: String!
password: String!
}
input UserCreateInput {
email: String!
fullName: String!
password: String!
}
input ProjectCreateInput {
name: String!
description: String
ownerId: String!
}
input TaskCreateInput {
title: String!
description: String
status: String
priority: String
projectId: String!
}
# Response Types
type AuthResponse {
token: String!
user: User!
}
type MessageResponse {
message: String!
success: Boolean!
}
# Queries
type Query {
me: User!
users: [User!]!
user(id: String!): User
projects: [Project!]!
project(id: String!): Project
tasks(projectId: String): [Task!]!
task(id: String!): Task
}
# Mutations
type Mutation {
login(input: LoginInput!): AuthResponse!
createUser(input: UserCreateInput!): User!
createProject(input: ProjectCreateInput!): Project!
createTask(input: TaskCreateInput!): Task!
}
`
}

View File

@@ -0,0 +1,100 @@
# Minimal GraphQL Schema for Phase 1
# Core Types
type User {
id: String!
email: String!
fullName: String
createdAt: String!
updatedAt: String!
}
type Project {
id: String!
name: String!
description: String
ownerId: String!
createdAt: String!
updatedAt: String!
}
type Task {
id: String!
title: String!
description: String
status: String!
priority: String!
projectId: String!
createdAt: String!
updatedAt: String!
}
# Input Types
input LoginInput {
email: String!
password: String!
}
input UserCreateInput {
email: String!
fullName: String!
password: String!
}
input ProjectCreateInput {
name: String!
description: String
ownerId: String!
}
input TaskCreateInput {
title: String!
description: String
status: String
priority: String
projectId: String!
}
# Response Types
type AuthResponse {
token: String!
user: User!
}
type MessageResponse {
message: String!
success: Boolean!
}
# Queries
type Query {
# Authentication
me: User!
# Users
users: [User!]!
user(id: String!): User
# Projects
projects: [Project!]!
project(id: String!): Project
# Tasks
tasks(projectId: String): [Task!]!
task(id: String!): Task
}
# Mutations
type Mutation {
# Authentication
login(input: LoginInput!): AuthResponse!
# Users
createUser(input: UserCreateInput!): User!
# Projects
createProject(input: ProjectCreateInput!): Project!
# Tasks
createTask(input: TaskCreateInput!): Task!
}

View File

@@ -0,0 +1,173 @@
package service
import (
"context"
"omega-server/local/model"
"omega-server/local/service"
)
// GraphQLService provides GraphQL-specific business logic
type GraphQLService struct {
membershipService *service.MembershipService
}
// NewGraphQLService creates a new GraphQL service
func NewGraphQLService(membershipService *service.MembershipService) *GraphQLService {
return &GraphQLService{
membershipService: membershipService,
}
}
// AuthResponse represents authentication response
type AuthResponse struct {
Token string `json:"token"`
User *model.User `json:"user"`
}
// MessageResponse represents a generic message response
type MessageResponse struct {
Message string `json:"message"`
Success bool `json:"success"`
}
// Login handles user authentication
func (s *GraphQLService) Login(ctx context.Context, email, password string) (*AuthResponse, error) {
token, err := s.membershipService.Login(ctx, email, password)
if err != nil {
return nil, err
}
// For now, return a mock user. In a full implementation, we'd get the user from the token
user := &model.User{
BaseModel: model.BaseModel{
ID: "mock-user-id",
},
Email: email,
FullName: "Mock User",
}
return &AuthResponse{
Token: token,
User: user,
}, nil
}
// CreateUser handles user creation
func (s *GraphQLService) CreateUser(ctx context.Context, email, password, fullName string) (*model.User, error) {
// Create domain model
user := &model.User{
Email: email,
FullName: fullName,
}
if err := user.SetPassword(password); err != nil {
return nil, err
}
return s.membershipService.CreateUser(ctx, user, []string{"user"})
}
// GetUsers retrieves all users
func (s *GraphQLService) GetUsers(ctx context.Context) ([]*model.User, error) {
return s.membershipService.ListUsers(ctx)
}
// GetUser retrieves a specific user by ID
func (s *GraphQLService) GetUser(ctx context.Context, id string) (*model.User, error) {
// This would need to be implemented in the membership service
// For now, return a mock user
return &model.User{
BaseModel: model.BaseModel{
ID: id,
},
Email: "mock@example.com",
FullName: "Mock User",
}, nil
}
// GetMe retrieves the current authenticated user
func (s *GraphQLService) GetMe(ctx context.Context, userID string) (*model.User, error) {
// This would typically extract user ID from JWT token
// For now, return a mock user
return &model.User{
BaseModel: model.BaseModel{
ID: userID,
},
Email: "current@example.com",
FullName: "Current User",
}, nil
}
// CreateProject handles project creation
func (s *GraphQLService) CreateProject(ctx context.Context, name, description, ownerID string) (*model.Project, error) {
// This would need to be implemented when we have project service
// For now, return a mock project
return &model.Project{
BaseModel: model.BaseModel{
ID: "mock-project-id",
},
Name: name,
Description: description,
OwnerID: ownerID,
}, nil
}
// GetProjects retrieves all projects
func (s *GraphQLService) GetProjects(ctx context.Context) ([]*model.Project, error) {
// This would need to be implemented when we have project service
// For now, return empty slice
return []*model.Project{}, nil
}
// GetProject retrieves a specific project by ID
func (s *GraphQLService) GetProject(ctx context.Context, id string) (*model.Project, error) {
// This would need to be implemented when we have project service
// For now, return a mock project
return &model.Project{
BaseModel: model.BaseModel{
ID: id,
},
Name: "Mock Project",
Description: "Mock project description",
OwnerID: "mock-owner-id",
}, nil
}
// CreateTask handles task creation
func (s *GraphQLService) CreateTask(ctx context.Context, title, description, status, priority, projectID string) (*model.Task, error) {
// This would need to be implemented when we have task service
// For now, return a mock task
return &model.Task{
BaseModel: model.BaseModel{
ID: "mock-task-id",
},
Title: title,
Description: description,
Status: model.TaskStatus(status),
Priority: model.TaskPriority(priority),
ProjectID: projectID,
}, nil
}
// GetTasks retrieves tasks, optionally filtered by project ID
func (s *GraphQLService) GetTasks(ctx context.Context, projectID *string) ([]*model.Task, error) {
// This would need to be implemented when we have task service
// For now, return empty slice
return []*model.Task{}, nil
}
// GetTask retrieves a specific task by ID
func (s *GraphQLService) GetTask(ctx context.Context, id string) (*model.Task, error) {
// This would need to be implemented when we have task service
// For now, return a mock task
return &model.Task{
BaseModel: model.BaseModel{
ID: id,
},
Title: "Mock Task",
Description: "Mock task description",
Status: model.TaskStatusTodo,
Priority: model.TaskPriorityMedium,
ProjectID: "mock-project-id",
}, nil
}

View File

@@ -26,13 +26,13 @@ type CachedUserInfo struct {
// AuthMiddleware provides authentication and permission middleware. // AuthMiddleware provides authentication and permission middleware.
type AuthMiddleware struct { type AuthMiddleware struct {
membershipService service.MembershipServiceInterface membershipService *service.MembershipService
cache *cache.InMemoryCache cache *cache.InMemoryCache
securityMW *security.SecurityMiddleware securityMW *security.SecurityMiddleware
} }
// NewAuthMiddleware creates a new AuthMiddleware. // NewAuthMiddleware creates a new AuthMiddleware.
func NewAuthMiddleware(ms service.MembershipServiceInterface, cache *cache.InMemoryCache) *AuthMiddleware { func NewAuthMiddleware(ms *service.MembershipService, cache *cache.InMemoryCache) *AuthMiddleware {
auth := &AuthMiddleware{ auth := &AuthMiddleware{
membershipService: ms, membershipService: ms,
cache: cache, cache: cache,
@@ -201,7 +201,7 @@ func (m *AuthMiddleware) getCachedUserInfo(ctx context.Context, userID string) (
userInfo := &CachedUserInfo{ userInfo := &CachedUserInfo{
UserID: userID, UserID: userID,
Username: user.Username, Username: user.FullName,
Roles: roleNames, Roles: roleNames,
RoleNames: roleNames, RoleNames: roleNames,
Permissions: permissions, Permissions: permissions,

View File

@@ -44,22 +44,22 @@ type AuditLogCreateRequest struct {
// AuditLogInfo represents public audit log information // AuditLogInfo represents public audit log information
type AuditLogInfo struct { type AuditLogInfo struct {
ID string `json:"id"` ID string `json:"id"`
UserID string `json:"userId"` UserID string `json:"userId"`
UserEmail string `json:"userEmail,omitempty"` UserEmail string `json:"userEmail,omitempty"`
UserName string `json:"userName,omitempty"` UserName string `json:"userName,omitempty"`
Action string `json:"action"` Action string `json:"action"`
Resource string `json:"resource"` Resource string `json:"resource"`
ResourceID string `json:"resourceId"` ResourceID string `json:"resourceId"`
Details map[string]interface{} `json:"details"` Details map[string]interface{} `json:"details"`
IPAddress string `json:"ipAddress"` IPAddress string `json:"ipAddress"`
UserAgent string `json:"userAgent"` UserAgent string `json:"userAgent"`
Success bool `json:"success"` Success bool `json:"success"`
ErrorMsg string `json:"errorMsg,omitempty"` ErrorMsg string `json:"errorMsg,omitempty"`
Duration int64 `json:"duration,omitempty"` Duration int64 `json:"duration,omitempty"`
SessionID string `json:"sessionId,omitempty"` SessionID string `json:"sessionId,omitempty"`
RequestID string `json:"requestId,omitempty"` RequestID string `json:"requestId,omitempty"`
DateCreated string `json:"dateCreated"` CreatedAt string `json:"created_at"`
} }
// BeforeCreate is called before creating an audit log // BeforeCreate is called before creating an audit log
@@ -122,26 +122,26 @@ func (al *AuditLog) GetDetailsAsJSON() (string, error) {
// ToAuditLogInfo converts AuditLog to AuditLogInfo (public information) // ToAuditLogInfo converts AuditLog to AuditLogInfo (public information)
func (al *AuditLog) ToAuditLogInfo() AuditLogInfo { func (al *AuditLog) ToAuditLogInfo() AuditLogInfo {
info := AuditLogInfo{ info := AuditLogInfo{
ID: al.ID, ID: al.ID,
UserID: al.UserID, UserID: al.UserID,
Action: al.Action, Action: al.Action,
Resource: al.Resource, Resource: al.Resource,
ResourceID: al.ResourceID, ResourceID: al.ResourceID,
Details: al.GetDetails(), Details: al.GetDetails(),
IPAddress: al.IPAddress, IPAddress: al.IPAddress,
UserAgent: al.UserAgent, UserAgent: al.UserAgent,
Success: al.Success, Success: al.Success,
ErrorMsg: al.ErrorMsg, ErrorMsg: al.ErrorMsg,
Duration: al.Duration, Duration: al.Duration,
SessionID: al.SessionID, SessionID: al.SessionID,
RequestID: al.RequestID, RequestID: al.RequestID,
DateCreated: al.DateCreated.Format("2006-01-02T15:04:05Z"), CreatedAt: al.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
} }
// Include user information if available // Include user information if available
if al.User != nil { if al.User != nil {
info.UserEmail = al.User.Email info.UserEmail = al.User.Email
info.UserName = al.User.Name info.UserName = al.User.FullName
} }
return info return info

View File

@@ -8,22 +8,22 @@ import (
// BaseModel provides common fields for all database models // BaseModel provides common fields for all database models
type BaseModel struct { type BaseModel struct {
ID string `json:"id" gorm:"primary_key;type:varchar(36)"` ID string `json:"id" gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
DateCreated time.Time `json:"dateCreated" gorm:"not null"` CreatedAt time.Time `json:"created_at" gorm:"not null;default:now()"`
DateUpdated time.Time `json:"dateUpdated" gorm:"not null"` UpdatedAt time.Time `json:"updated_at" gorm:"not null;default:now()"`
} }
// Init initializes base model with DateCreated, DateUpdated, and ID values // Init initializes base model with CreatedAt, UpdatedAt, and ID values
func (bm *BaseModel) Init() { func (bm *BaseModel) Init() {
now := time.Now().UTC() now := time.Now().UTC()
bm.ID = uuid.NewString() bm.ID = uuid.NewString()
bm.DateCreated = now bm.CreatedAt = now
bm.DateUpdated = now bm.UpdatedAt = now
} }
// UpdateTimestamp updates the DateUpdated field // UpdateTimestamp updates the UpdatedAt field
func (bm *BaseModel) UpdateTimestamp() { func (bm *BaseModel) UpdateTimestamp() {
bm.DateUpdated = time.Now().UTC() bm.UpdatedAt = time.Now().UTC()
} }
// BeforeCreate is a GORM hook that runs before creating a record // BeforeCreate is a GORM hook that runs before creating a record
@@ -76,7 +76,7 @@ func DefaultParams() Params {
return Params{ return Params{
Page: 1, Page: 1,
Limit: 10, Limit: 10,
SortBy: "dateCreated", SortBy: "created_at",
SortOrder: "desc", SortOrder: "desc",
} }
} }
@@ -90,7 +90,7 @@ func (p *Params) Validate() {
p.Limit = 10 p.Limit = 10
} }
if p.SortBy == "" { if p.SortBy == "" {
p.SortBy = "dateCreated" p.SortBy = "created_at"
} }
if p.SortOrder != "asc" && p.SortOrder != "desc" { if p.SortOrder != "asc" && p.SortOrder != "desc" {
p.SortOrder = "desc" p.SortOrder = "desc"

144
local/model/integration.go Normal file
View File

@@ -0,0 +1,144 @@
package model
import (
"encoding/json"
"errors"
"strings"
"gorm.io/gorm"
)
// Integration represents a third-party integration configuration
type Integration struct {
BaseModel
ProjectID string `json:"project_id" gorm:"not null;type:uuid;index;references:projects(id);onDelete:CASCADE"`
Type string `json:"type" gorm:"not null;type:varchar(50)"`
Config json.RawMessage `json:"config" gorm:"type:jsonb;not null"`
Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"`
}
// IntegrationCreateRequest represents the request to create a new integration
type IntegrationCreateRequest struct {
ProjectID string `json:"project_id" validate:"required,uuid"`
Type string `json:"type" validate:"required,min=1,max=50"`
Config map[string]interface{} `json:"config" validate:"required"`
}
// IntegrationUpdateRequest represents the request to update an integration
type IntegrationUpdateRequest struct {
Config map[string]interface{} `json:"config" validate:"required"`
}
// IntegrationInfo represents public integration information
type IntegrationInfo struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
Type string `json:"type"`
Config map[string]interface{} `json:"config"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// BeforeCreate is called before creating an integration
func (i *Integration) BeforeCreate(tx *gorm.DB) error {
i.BaseModel.BeforeCreate()
// Normalize type
i.Type = strings.ToLower(strings.TrimSpace(i.Type))
return i.Validate()
}
// BeforeUpdate is called before updating an integration
func (i *Integration) BeforeUpdate(tx *gorm.DB) error {
i.BaseModel.BeforeUpdate()
// Normalize type if it's being updated
if i.Type != "" {
i.Type = strings.ToLower(strings.TrimSpace(i.Type))
}
return i.Validate()
}
// Validate validates integration data
func (i *Integration) Validate() error {
if i.ProjectID == "" {
return errors.New("project_id is required")
}
if i.Type == "" {
return errors.New("type is required")
}
if len(i.Type) > 50 {
return errors.New("type must not exceed 50 characters")
}
if len(i.Config) == 0 {
return errors.New("config is required")
}
// Validate that config is valid JSON
var configMap map[string]interface{}
if err := json.Unmarshal(i.Config, &configMap); err != nil {
return errors.New("config must be valid JSON")
}
return nil
}
// ToIntegrationInfo converts Integration to IntegrationInfo (public information)
func (i *Integration) ToIntegrationInfo() IntegrationInfo {
integrationInfo := IntegrationInfo{
ID: i.ID,
ProjectID: i.ProjectID,
Type: i.Type,
CreatedAt: i.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: i.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
// Parse config JSON to map
var configMap map[string]interface{}
if err := json.Unmarshal(i.Config, &configMap); err == nil {
integrationInfo.Config = configMap
}
return integrationInfo
}
// SetConfig sets the configuration from a map
func (i *Integration) SetConfig(config map[string]interface{}) error {
configJSON, err := json.Marshal(config)
if err != nil {
return err
}
i.Config = configJSON
return nil
}
// GetConfig gets the configuration as a map
func (i *Integration) GetConfig() (map[string]interface{}, error) {
var configMap map[string]interface{}
err := json.Unmarshal(i.Config, &configMap)
return configMap, err
}
// GetConfigValue gets a specific configuration value
func (i *Integration) GetConfigValue(key string) (interface{}, error) {
configMap, err := i.GetConfig()
if err != nil {
return nil, err
}
return configMap[key], nil
}
// SetConfigValue sets a specific configuration value
func (i *Integration) SetConfigValue(key string, value interface{}) error {
configMap, err := i.GetConfig()
if err != nil {
return err
}
configMap[key] = value
return i.SetConfig(configMap)
}

View File

@@ -156,10 +156,10 @@ func (f *MembershipFilter) GetSorting() (field string, desc bool) {
// Map common sort fields to database column names // Map common sort fields to database column names
switch f.SortBy { switch f.SortBy {
case "dateCreated": case "created_at":
field = "date_created" field = "created_at"
case "dateUpdated": case "updated_at":
field = "date_updated" field = "updated_at"
case "username": case "username":
field = "username" field = "username"
case "email": case "email":

View File

@@ -42,7 +42,7 @@ type PermissionInfo struct {
Active bool `json:"active"` Active bool `json:"active"`
System bool `json:"system"` System bool `json:"system"`
RoleCount int64 `json:"roleCount"` RoleCount int64 `json:"roleCount"`
DateCreated string `json:"dateCreated"` CreatedAt string `json:"created_at"`
} }
// BeforeCreate is called before creating a permission // BeforeCreate is called before creating a permission
@@ -132,7 +132,7 @@ func (p *Permission) ToPermissionInfo() PermissionInfo {
Category: p.Category, Category: p.Category,
Active: p.Active, Active: p.Active,
System: p.System, System: p.System,
DateCreated: p.DateCreated.Format("2006-01-02T15:04:05Z"), CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
} }
} }

148
local/model/project.go Normal file
View File

@@ -0,0 +1,148 @@
package model
import (
"errors"
"strings"
"gorm.io/gorm"
)
// Project represents a project in the system
type Project struct {
BaseModel
Name string `json:"name" gorm:"not null;type:varchar(255)"`
Description string `json:"description" gorm:"type:text"`
OwnerID string `json:"owner_id" gorm:"not null;type:uuid;index;references:users(id)"`
TypeID string `json:"type_id" gorm:"not null;type:uuid;index;references:types(id)"`
Owner User `json:"owner,omitempty" gorm:"foreignKey:OwnerID"`
Type Type `json:"type,omitempty" gorm:"foreignKey:TypeID"`
Tasks []Task `json:"tasks,omitempty" gorm:"foreignKey:ProjectID"`
Members []User `json:"members,omitempty" gorm:"many2many:project_members;"`
}
// ProjectCreateRequest represents the request to create a new project
type ProjectCreateRequest struct {
Name string `json:"name" validate:"required,min=1,max=255"`
Description string `json:"description" validate:"max=1000"`
OwnerID string `json:"owner_id" validate:"required,uuid"`
TypeID string `json:"type_id" validate:"required,uuid"`
}
// ProjectUpdateRequest represents the request to update a project
type ProjectUpdateRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
TypeID *string `json:"type_id,omitempty" validate:"omitempty,uuid"`
}
// ProjectInfo represents public project information
type ProjectInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
OwnerID string `json:"owner_id"`
TypeID string `json:"type_id"`
Owner UserInfo `json:"owner,omitempty"`
Type TypeInfo `json:"type,omitempty"`
TaskCount int64 `json:"task_count"`
MemberCount int64 `json:"member_count"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// ProjectMember represents the many-to-many relationship between projects and users
type ProjectMember struct {
ProjectID string `json:"project_id" gorm:"type:uuid;primaryKey"`
UserID string `json:"user_id" gorm:"type:uuid;primaryKey"`
RoleID string `json:"role_id" gorm:"type:uuid;not null;references:roles(id)"`
Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"`
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Role Role `json:"role,omitempty" gorm:"foreignKey:RoleID"`
}
// BeforeCreate is called before creating a project
func (p *Project) BeforeCreate(tx *gorm.DB) error {
p.BaseModel.BeforeCreate()
// Normalize name and description
p.Name = strings.TrimSpace(p.Name)
p.Description = strings.TrimSpace(p.Description)
return p.Validate()
}
// BeforeUpdate is called before updating a project
func (p *Project) BeforeUpdate(tx *gorm.DB) error {
p.BaseModel.BeforeUpdate()
// Normalize fields if they're being updated
if p.Name != "" {
p.Name = strings.TrimSpace(p.Name)
}
if p.Description != "" {
p.Description = strings.TrimSpace(p.Description)
}
return p.Validate()
}
// Validate validates project data
func (p *Project) Validate() error {
if p.Name == "" {
return errors.New("name is required")
}
if len(p.Name) > 255 {
return errors.New("name must not exceed 255 characters")
}
if p.OwnerID == "" {
return errors.New("owner_id is required")
}
if p.TypeID == "" {
return errors.New("type_id is required")
}
return nil
}
// ToProjectInfo converts Project to ProjectInfo (public information)
func (p *Project) ToProjectInfo() ProjectInfo {
projectInfo := ProjectInfo{
ID: p.ID,
Name: p.Name,
Description: p.Description,
OwnerID: p.OwnerID,
TypeID: p.TypeID,
CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: p.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
// Add owner info if loaded
if p.Owner.ID != "" {
projectInfo.Owner = p.Owner.ToUserInfo()
}
// Add type info if loaded
if p.Type.ID != "" {
projectInfo.Type = p.Type.ToTypeInfo()
}
return projectInfo
}
// HasMember checks if a user is a member of the project
func (p *Project) HasMember(userID string) bool {
for _, member := range p.Members {
if member.ID == userID {
return true
}
}
return false
}
// IsOwner checks if a user is the owner of the project
func (p *Project) IsOwner(userID string) bool {
return p.OwnerID == userID
}

View File

@@ -42,7 +42,7 @@ type RoleInfo struct {
System bool `json:"system"` System bool `json:"system"`
Permissions []PermissionInfo `json:"permissions"` Permissions []PermissionInfo `json:"permissions"`
UserCount int64 `json:"userCount"` UserCount int64 `json:"userCount"`
DateCreated string `json:"dateCreated"` CreatedAt string `json:"created_at"`
} }
// BeforeCreate is called before creating a role // BeforeCreate is called before creating a role
@@ -120,7 +120,7 @@ func (r *Role) ToRoleInfo() RoleInfo {
Active: r.Active, Active: r.Active,
System: r.System, System: r.System,
Permissions: make([]PermissionInfo, len(r.Permissions)), Permissions: make([]PermissionInfo, len(r.Permissions)),
DateCreated: r.DateCreated.Format("2006-01-02T15:04:05Z"), CreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
} }
// Convert permissions // Convert permissions

View File

@@ -86,7 +86,7 @@ type SecurityEventInfo struct {
ResolverName string `json:"resolverName,omitempty"` ResolverName string `json:"resolverName,omitempty"`
ResolvedAt *time.Time `json:"resolvedAt,omitempty"` ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
Notes string `json:"notes,omitempty"` Notes string `json:"notes,omitempty"`
DateCreated string `json:"dateCreated"` CreatedAt string `json:"created_at"`
} }
// BeforeCreate is called before creating a security event // BeforeCreate is called before creating a security event
@@ -202,19 +202,19 @@ func (se *SecurityEvent) ToSecurityEventInfo() SecurityEventInfo {
ResolvedBy: se.ResolvedBy, ResolvedBy: se.ResolvedBy,
ResolvedAt: se.ResolvedAt, ResolvedAt: se.ResolvedAt,
Notes: se.Notes, Notes: se.Notes,
DateCreated: se.DateCreated.Format("2006-01-02T15:04:05Z"), CreatedAt: se.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
} }
// Include user information if available // Include user information if available
if se.User != nil { if se.User != nil {
info.UserEmail = se.User.Email info.UserEmail = se.User.Email
info.UserName = se.User.Name info.UserName = se.User.FullName
} }
// Include resolver information if available // Include resolver information if available
if se.Resolver != nil { if se.Resolver != nil {
info.ResolverEmail = se.Resolver.Email info.ResolverEmail = se.Resolver.Email
info.ResolverName = se.Resolver.Name info.ResolverName = se.Resolver.FullName
} }
return info return info

View File

@@ -56,7 +56,7 @@ type SystemConfigInfo struct {
DataType string `json:"dataType"` DataType string `json:"dataType"`
IsEditable bool `json:"isEditable"` IsEditable bool `json:"isEditable"`
IsSecret bool `json:"isSecret"` IsSecret bool `json:"isSecret"`
DateCreated string `json:"dateCreated"` CreatedAt string `json:"created_at"`
DateModified string `json:"dateModified"` DateModified string `json:"dateModified"`
} }
@@ -170,7 +170,7 @@ func (sc *SystemConfig) ToSystemConfigInfo() SystemConfigInfo {
DataType: sc.DataType, DataType: sc.DataType,
IsEditable: sc.IsEditable, IsEditable: sc.IsEditable,
IsSecret: sc.IsSecret, IsSecret: sc.IsSecret,
DateCreated: sc.DateCreated.Format("2006-01-02T15:04:05Z"), CreatedAt: sc.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
DateModified: sc.DateModified, DateModified: sc.DateModified,
} }

213
local/model/task.go Normal file
View File

@@ -0,0 +1,213 @@
package model
import (
"errors"
"strings"
"gorm.io/gorm"
)
// TaskStatus represents the status of a task
type TaskStatus string
const (
TaskStatusTodo TaskStatus = "todo"
TaskStatusInProgress TaskStatus = "in_progress"
TaskStatusDone TaskStatus = "done"
TaskStatusCanceled TaskStatus = "canceled"
)
// TaskPriority represents the priority of a task
type TaskPriority string
const (
TaskPriorityLow TaskPriority = "low"
TaskPriorityMedium TaskPriority = "medium"
TaskPriorityHigh TaskPriority = "high"
)
// Task represents a task in the system
type Task struct {
BaseModel
Title string `json:"title" gorm:"not null;type:varchar(255)"`
Description string `json:"description" gorm:"type:text"`
Status TaskStatus `json:"status" gorm:"not null;default:'todo';type:varchar(20)"`
Priority TaskPriority `json:"priority" gorm:"not null;default:'medium';type:varchar(20)"`
ProjectID string `json:"project_id" gorm:"not null;type:uuid;index;references:projects(id);onDelete:CASCADE"`
DueDate *string `json:"due_date" gorm:"type:date"`
Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"`
Assignees []User `json:"assignees,omitempty" gorm:"many2many:task_assignees;"`
}
// TaskCreateRequest represents the request to create a new task
type TaskCreateRequest struct {
Title string `json:"title" validate:"required,min=1,max=255"`
Description string `json:"description" validate:"max=1000"`
Status TaskStatus `json:"status" validate:"omitempty,oneof=todo in_progress done canceled"`
Priority TaskPriority `json:"priority" validate:"omitempty,oneof=low medium high"`
ProjectID string `json:"project_id" validate:"required,uuid"`
DueDate *string `json:"due_date"`
AssigneeIDs []string `json:"assignee_ids"`
}
// TaskUpdateRequest represents the request to update a task
type TaskUpdateRequest struct {
Title *string `json:"title,omitempty" validate:"omitempty,min=1,max=255"`
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
Status *TaskStatus `json:"status,omitempty" validate:"omitempty,oneof=todo in_progress done canceled"`
Priority *TaskPriority `json:"priority,omitempty" validate:"omitempty,oneof=low medium high"`
DueDate *string `json:"due_date,omitempty"`
AssigneeIDs []string `json:"assignee_ids,omitempty"`
}
// TaskInfo represents public task information
type TaskInfo struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status TaskStatus `json:"status"`
Priority TaskPriority `json:"priority"`
ProjectID string `json:"project_id"`
DueDate *string `json:"due_date"`
Project ProjectInfo `json:"project,omitempty"`
Assignees []UserInfo `json:"assignees,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// TaskAssignee represents the many-to-many relationship between tasks and users
type TaskAssignee struct {
TaskID string `json:"task_id" gorm:"type:uuid;primaryKey"`
UserID string `json:"user_id" gorm:"type:uuid;primaryKey"`
Task Task `json:"task,omitempty" gorm:"foreignKey:TaskID"`
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// BeforeCreate is called before creating a task
func (t *Task) BeforeCreate(tx *gorm.DB) error {
t.BaseModel.BeforeCreate()
// Normalize title and description
t.Title = strings.TrimSpace(t.Title)
t.Description = strings.TrimSpace(t.Description)
// Set default values
if t.Status == "" {
t.Status = TaskStatusTodo
}
if t.Priority == "" {
t.Priority = TaskPriorityMedium
}
return t.Validate()
}
// BeforeUpdate is called before updating a task
func (t *Task) BeforeUpdate(tx *gorm.DB) error {
t.BaseModel.BeforeUpdate()
// Normalize fields if they're being updated
if t.Title != "" {
t.Title = strings.TrimSpace(t.Title)
}
if t.Description != "" {
t.Description = strings.TrimSpace(t.Description)
}
return t.Validate()
}
// Validate validates task data
func (t *Task) Validate() error {
if t.Title == "" {
return errors.New("title is required")
}
if len(t.Title) > 255 {
return errors.New("title must not exceed 255 characters")
}
if t.ProjectID == "" {
return errors.New("project_id is required")
}
// Validate status
if t.Status != "" {
switch t.Status {
case TaskStatusTodo, TaskStatusInProgress, TaskStatusDone, TaskStatusCanceled:
// Valid status
default:
return errors.New("invalid status")
}
}
// Validate priority
if t.Priority != "" {
switch t.Priority {
case TaskPriorityLow, TaskPriorityMedium, TaskPriorityHigh:
// Valid priority
default:
return errors.New("invalid priority")
}
}
return nil
}
// ToTaskInfo converts Task to TaskInfo (public information)
func (t *Task) ToTaskInfo() TaskInfo {
taskInfo := TaskInfo{
ID: t.ID,
Title: t.Title,
Description: t.Description,
Status: t.Status,
Priority: t.Priority,
ProjectID: t.ProjectID,
DueDate: t.DueDate,
CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: t.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
Assignees: make([]UserInfo, len(t.Assignees)),
}
// Add project info if loaded
if t.Project.ID != "" {
taskInfo.Project = t.Project.ToProjectInfo()
}
// Add assignee info if loaded
for i, assignee := range t.Assignees {
taskInfo.Assignees[i] = assignee.ToUserInfo()
}
return taskInfo
}
// IsAssignedTo checks if a user is assigned to this task
func (t *Task) IsAssignedTo(userID string) bool {
for _, assignee := range t.Assignees {
if assignee.ID == userID {
return true
}
}
return false
}
// IsCompleted checks if the task is completed
func (t *Task) IsCompleted() bool {
return t.Status == TaskStatusDone
}
// IsCanceled checks if the task is canceled
func (t *Task) IsCanceled() bool {
return t.Status == TaskStatusCanceled
}
// IsInProgress checks if the task is in progress
func (t *Task) IsInProgress() bool {
return t.Status == TaskStatusInProgress
}
// IsTodo checks if the task is todo
func (t *Task) IsTodo() bool {
return t.Status == TaskStatusTodo
}

95
local/model/type.go Normal file
View File

@@ -0,0 +1,95 @@
package model
import (
"errors"
"strings"
"gorm.io/gorm"
)
// Type represents a project type in the system
type Type struct {
BaseModel
UserID *string `json:"user_id" gorm:"type:uuid;index;references:users(id);onDelete:SET NULL"`
Name string `json:"name" gorm:"not null;type:varchar(100)"`
Description string `json:"description" gorm:"type:text"`
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// TypeCreateRequest represents the request to create a new type
type TypeCreateRequest struct {
UserID *string `json:"user_id"`
Name string `json:"name" validate:"required,min=1,max=100"`
Description string `json:"description" validate:"max=1000"`
}
// TypeUpdateRequest represents the request to update a type
type TypeUpdateRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=100"`
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
}
// TypeInfo represents public type information
type TypeInfo struct {
ID string `json:"id"`
UserID *string `json:"user_id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// BeforeCreate is called before creating a type
func (t *Type) BeforeCreate(tx *gorm.DB) error {
t.BaseModel.BeforeCreate()
// Normalize name
t.Name = strings.TrimSpace(t.Name)
t.Description = strings.TrimSpace(t.Description)
return t.Validate()
}
// BeforeUpdate is called before updating a type
func (t *Type) BeforeUpdate(tx *gorm.DB) error {
t.BaseModel.BeforeUpdate()
// Normalize fields if they're being updated
if t.Name != "" {
t.Name = strings.TrimSpace(t.Name)
}
if t.Description != "" {
t.Description = strings.TrimSpace(t.Description)
}
return t.Validate()
}
// Validate validates type data
func (t *Type) Validate() error {
if t.Name == "" {
return errors.New("name is required")
}
if len(t.Name) > 100 {
return errors.New("name must not exceed 100 characters")
}
if len(t.Description) > 1000 {
return errors.New("description must not exceed 1000 characters")
}
return nil
}
// ToTypeInfo converts Type to TypeInfo (public information)
func (t *Type) ToTypeInfo() TypeInfo {
return TypeInfo{
ID: t.ID,
UserID: t.UserID,
Name: t.Name,
Description: t.Description,
CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: t.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}

View File

@@ -13,42 +13,98 @@ import (
// User represents a user in the system // User represents a user in the system
type User struct { type User struct {
BaseModel BaseModel
Email string `json:"email" gorm:"unique;not null;type:varchar(255)"` Email string `json:"email" gorm:"unique;not null;type:varchar(255)"`
Username string `json:"username" gorm:"unique;not null;type:varchar(100)"` PasswordHash string `json:"-" gorm:"not null;type:varchar(255)"`
Name string `json:"name" gorm:"not null;type:varchar(255)"` FullName string `json:"full_name" gorm:"type:varchar(255)"`
PasswordHash string `json:"-" gorm:"not null;type:text"` Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
Active bool `json:"active" gorm:"default:true"`
EmailVerified bool `json:"emailVerified" gorm:"default:false"`
EmailVerificationToken string `json:"-" gorm:"type:varchar(255)"`
PasswordResetToken string `json:"-" gorm:"type:varchar(255)"`
PasswordResetExpires *time.Time `json:"-"`
LastLogin *time.Time `json:"lastLogin"`
LoginAttempts int `json:"-" gorm:"default:0"`
LockedUntil *time.Time `json:"-"`
TwoFactorEnabled bool `json:"twoFactorEnabled" gorm:"default:false"`
TwoFactorSecret string `json:"-" gorm:"type:varchar(255)"`
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
AuditLogs []AuditLog `json:"-" gorm:"foreignKey:UserID"`
} }
// UserCreateRequest represents the request to create a new user // UserCreateRequest represents the request to create a new user
type UserCreateRequest struct { type UserCreateRequest struct {
Email string `json:"email" validate:"required,email"` Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,min=3,max=50"` FullName string `json:"full_name" validate:"required,min=2,max=100"`
Name string `json:"name" validate:"required,min=2,max=100"`
Password string `json:"password" validate:"required,min=8"` Password string `json:"password" validate:"required,min=8"`
RoleIDs []string `json:"roleIds"` RoleIDs []string `json:"roleIds"`
} }
// ToUser converts UserCreateRequest to User domain model
func (req *UserCreateRequest) ToUser() (*User, error) {
user := &User{
Email: req.Email,
FullName: req.FullName,
}
// Handle password hashing
if err := user.SetPassword(req.Password); err != nil {
return nil, err
}
// Note: Roles will be set by the service layer after validation
return user, nil
}
// Validate validates the UserCreateRequest
func (req *UserCreateRequest) Validate() error {
if req.Email == "" {
return errors.New("email is required")
}
if !isValidEmail(req.Email) {
return errors.New("invalid email format")
}
if len(req.Password) < 8 {
return errors.New("password must be at least 8 characters")
}
if req.FullName == "" {
return errors.New("full name is required")
}
return nil
}
// UserUpdateRequest represents the request to update a user // UserUpdateRequest represents the request to update a user
type UserUpdateRequest struct { type UserUpdateRequest struct {
Email *string `json:"email,omitempty" validate:"omitempty,email"` Email *string `json:"email,omitempty" validate:"omitempty,email"`
Username *string `json:"username,omitempty" validate:"omitempty,min=3,max=50"` FullName *string `json:"full_name,omitempty" validate:"omitempty,min=2,max=100"`
Name *string `json:"name,omitempty" validate:"omitempty,min=2,max=100"`
Active *bool `json:"active,omitempty"`
RoleIDs []string `json:"roleIds,omitempty"` RoleIDs []string `json:"roleIds,omitempty"`
} }
// ApplyToUser applies the UserUpdateRequest to an existing User
func (req *UserUpdateRequest) ApplyToUser(user *User) error {
if req.Email != nil {
user.Email = *req.Email
}
if req.FullName != nil {
user.FullName = *req.FullName
}
// Note: Roles will be handled by the service layer
return nil
}
// Validate validates the UserUpdateRequest
func (req *UserUpdateRequest) Validate() error {
if req.Email != nil && !isValidEmail(*req.Email) {
return errors.New("invalid email format")
}
if req.FullName != nil && len(*req.FullName) == 0 {
return errors.New("full name cannot be empty")
}
if req.FullName != nil && len(*req.FullName) > 255 {
return errors.New("full name must not exceed 255 characters")
}
return nil
}
// UserResponse represents the response when returning user data
type UserResponse struct {
ID string `json:"id"`
Email string `json:"email"`
FullName string `json:"fullName"`
Roles []string `json:"roles"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// UserLoginRequest represents a login request // UserLoginRequest represents a login request
type UserLoginRequest struct { type UserLoginRequest struct {
Email string `json:"email" validate:"required,email"` Email string `json:"email" validate:"required,email"`
@@ -65,16 +121,13 @@ type UserLoginResponse struct {
// UserInfo represents public user information // UserInfo represents public user information
type UserInfo struct { type UserInfo struct {
ID string `json:"id"` ID string `json:"id"`
Email string `json:"email"` Email string `json:"email"`
Username string `json:"username"` FullName string `json:"full_name"`
Name string `json:"name"` Roles []RoleInfo `json:"roles"`
Active bool `json:"active"` Permissions []string `json:"permissions"`
EmailVerified bool `json:"emailVerified"` CreatedAt time.Time `json:"created_at"`
LastLogin *time.Time `json:"lastLogin"` UpdatedAt time.Time `json:"updated_at"`
Roles []RoleInfo `json:"roles"`
Permissions []string `json:"permissions"`
DateCreated time.Time `json:"dateCreated"`
} }
// ChangePasswordRequest represents a password change request // ChangePasswordRequest represents a password change request
@@ -98,10 +151,9 @@ type ResetPasswordConfirmRequest struct {
func (u *User) BeforeCreate(tx *gorm.DB) error { func (u *User) BeforeCreate(tx *gorm.DB) error {
u.BaseModel.BeforeCreate() u.BaseModel.BeforeCreate()
// Normalize email and username // Normalize email and full name
u.Email = strings.ToLower(strings.TrimSpace(u.Email)) u.Email = strings.ToLower(strings.TrimSpace(u.Email))
u.Username = strings.ToLower(strings.TrimSpace(u.Username)) u.FullName = strings.TrimSpace(u.FullName)
u.Name = strings.TrimSpace(u.Name)
return u.Validate() return u.Validate()
} }
@@ -114,11 +166,8 @@ func (u *User) BeforeUpdate(tx *gorm.DB) error {
if u.Email != "" { if u.Email != "" {
u.Email = strings.ToLower(strings.TrimSpace(u.Email)) u.Email = strings.ToLower(strings.TrimSpace(u.Email))
} }
if u.Username != "" { if u.FullName != "" {
u.Username = strings.ToLower(strings.TrimSpace(u.Username)) u.FullName = strings.TrimSpace(u.FullName)
}
if u.Name != "" {
u.Name = strings.TrimSpace(u.Name)
} }
return u.Validate() return u.Validate()
@@ -134,24 +183,8 @@ func (u *User) Validate() error {
return errors.New("invalid email format") return errors.New("invalid email format")
} }
if u.Username == "" { if u.FullName != "" && len(u.FullName) > 255 {
return errors.New("username is required") return errors.New("full name must not exceed 255 characters")
}
if len(u.Username) < 3 || len(u.Username) > 50 {
return errors.New("username must be between 3 and 50 characters")
}
if !isValidUsername(u.Username) {
return errors.New("username can only contain letters, numbers, underscores, and hyphens")
}
if u.Name == "" {
return errors.New("name is required")
}
if len(u.Name) < 2 || len(u.Name) > 100 {
return errors.New("name must be between 2 and 100 characters")
} }
return nil return nil
@@ -182,55 +215,16 @@ func (u *User) VerifyPassword(plainPassword string) bool {
return u.CheckPassword(plainPassword) return u.CheckPassword(plainPassword)
} }
// IsLocked checks if the user account is locked
func (u *User) IsLocked() bool {
if u.LockedUntil == nil {
return false
}
return time.Now().Before(*u.LockedUntil)
}
// Lock locks the user account for the specified duration
func (u *User) Lock(duration time.Duration) {
lockUntil := time.Now().Add(duration)
u.LockedUntil = &lockUntil
}
// Unlock unlocks the user account
func (u *User) Unlock() {
u.LockedUntil = nil
u.LoginAttempts = 0
}
// IncrementLoginAttempts increments the login attempt counter
func (u *User) IncrementLoginAttempts() {
u.LoginAttempts++
}
// ResetLoginAttempts resets the login attempt counter
func (u *User) ResetLoginAttempts() {
u.LoginAttempts = 0
}
// UpdateLastLogin updates the last login timestamp
func (u *User) UpdateLastLogin() {
now := time.Now()
u.LastLogin = &now
}
// ToUserInfo converts User to UserInfo (public information) // ToUserInfo converts User to UserInfo (public information)
func (u *User) ToUserInfo() UserInfo { func (u *User) ToUserInfo() UserInfo {
userInfo := UserInfo{ userInfo := UserInfo{
ID: u.ID, ID: u.ID,
Email: u.Email, Email: u.Email,
Username: u.Username, FullName: u.FullName,
Name: u.Name, CreatedAt: u.CreatedAt,
Active: u.Active, UpdatedAt: u.UpdatedAt,
EmailVerified: u.EmailVerified, Roles: make([]RoleInfo, len(u.Roles)),
LastLogin: u.LastLogin, Permissions: []string{},
DateCreated: u.DateCreated,
Roles: make([]RoleInfo, len(u.Roles)),
Permissions: []string{},
} }
// Convert roles and collect permissions // Convert roles and collect permissions
@@ -250,6 +244,23 @@ func (u *User) ToUserInfo() UserInfo {
return userInfo return userInfo
} }
// ToResponse converts User to UserResponse (for API responses)
func (u *User) ToResponse() *UserResponse {
roleNames := make([]string, len(u.Roles))
for i, role := range u.Roles {
roleNames[i] = role.Name
}
return &UserResponse{
ID: u.ID,
Email: u.Email,
FullName: u.FullName,
Roles: roleNames,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// HasRole checks if the user has a specific role // HasRole checks if the user has a specific role
func (u *User) HasRole(roleName string) bool { func (u *User) HasRole(roleName string) bool {
for _, role := range u.Roles { for _, role := range u.Roles {
@@ -278,12 +289,6 @@ func isValidEmail(email string) bool {
return emailRegex.MatchString(email) return emailRegex.MatchString(email)
} }
// isValidUsername validates username format
func isValidUsername(username string) bool {
usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
return usernameRegex.MatchString(username)
}
// validatePassword validates password strength // validatePassword validates password strength
func validatePassword(password string) error { func validatePassword(password string) error {
if len(password) < 8 { if len(password) < 8 {
@@ -294,25 +299,5 @@ func validatePassword(password string) error {
return errors.New("password must not exceed 128 characters") return errors.New("password must not exceed 128 characters")
} }
// Check for at least one lowercase letter
if matched, _ := regexp.MatchString(`[a-z]`, password); !matched {
return errors.New("password must contain at least one lowercase letter")
}
// Check for at least one uppercase letter
if matched, _ := regexp.MatchString(`[A-Z]`, password); !matched {
return errors.New("password must contain at least one uppercase letter")
}
// Check for at least one digit
if matched, _ := regexp.MatchString(`\d`, password); !matched {
return errors.New("password must contain at least one digit")
}
// Check for at least one special character
if matched, _ := regexp.MatchString(`[!@#$%^&*(),.?":{}|<>]`, password); !matched {
return errors.New("password must contain at least one special character")
}
return nil return nil
} }

View File

@@ -22,12 +22,12 @@ func NewMembershipRepository(db *gorm.DB) *MembershipRepository {
} }
} }
// FindUserByUsername finds a user by their username. // FindUserByEmail finds a user by their email.
// It preloads the user's role and the role's permissions. // It preloads the user's role and the role's permissions.
func (r *MembershipRepository) FindUserByUsername(ctx context.Context, username string) (*model.User, error) { func (r *MembershipRepository) FindUserByEmail(ctx context.Context, email string) (*model.User, error) {
var user model.User var user model.User
db := r.db.WithContext(ctx) db := r.db.WithContext(ctx)
err := db.Preload("Roles.Permissions").Where("username = ?", username).First(&user).Error err := db.Preload("Roles.Permissions").Where("email = ?", email).First(&user).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -8,6 +8,7 @@ import (
"omega-server/local/utl/jwt" "omega-server/local/utl/jwt"
"omega-server/local/utl/logging" "omega-server/local/utl/logging"
"os" "os"
"strings"
"github.com/google/uuid" "github.com/google/uuid"
) )
@@ -38,8 +39,8 @@ func (s *MembershipService) SetCacheInvalidator(invalidator CacheInvalidator) {
} }
// Login authenticates a user and returns a JWT. // Login authenticates a user and returns a JWT.
func (s *MembershipService) Login(ctx context.Context, username, password string) (string, error) { func (s *MembershipService) Login(ctx context.Context, email, password string) (string, error) {
user, err := s.repo.FindUserByUsername(ctx, username) user, err := s.repo.FindUserByEmail(ctx, email)
if err != nil { if err != nil {
return "", errors.New("invalid credentials") return "", errors.New("invalid credentials")
} }
@@ -55,38 +56,42 @@ func (s *MembershipService) Login(ctx context.Context, username, password string
roleNames[i] = role.Name roleNames[i] = role.Name
} }
return jwt.GenerateToken(user.ID, user.Email, user.Username, roleNames) return jwt.GenerateToken(user.ID, user.Email, user.FullName, roleNames)
} }
// CreateUser creates a new user. // CreateUser creates a new user.
func (s *MembershipService) CreateUser(ctx context.Context, username, password, roleName string) (*model.User, error) { func (s *MembershipService) CreateUser(ctx context.Context, user *model.User, roleIDs []string) (*model.User, error) {
// Validate domain model
role, err := s.repo.FindRoleByName(ctx, roleName) if err := user.Validate(); err != nil {
if err != nil {
logging.Error("Failed to find role by name: %v", err)
return nil, errors.New("role not found")
}
user := &model.User{
Username: username,
Email: username + "@example.com", // You may want to accept email as parameter
Name: username,
}
// Set password using the model's SetPassword method
if err := user.SetPassword(password); err != nil {
return nil, err return nil, err
} }
// Assign roles // Handle roles
user.Roles = []model.Role{*role} if len(roleIDs) > 0 {
roles := make([]model.Role, 0, len(roleIDs))
for _, roleID := range roleIDs {
role, err := s.repo.FindRoleByName(ctx, roleID)
if err != nil {
logging.Error("Failed to find role by name: %v", err)
return nil, errors.New("role not found: " + roleID)
}
roles = append(roles, *role)
}
user.Roles = roles
}
// Create user
if err := s.repo.CreateUser(ctx, user); err != nil { if err := s.repo.CreateUser(ctx, user); err != nil {
logging.Error("Failed to create user: %v", err) logging.Error("Failed to create user: %v", err)
return nil, err return nil, err
} }
logging.InfoOperation("USER_CREATE", "Created user: "+user.Username+" (ID: "+user.ID+", Role: "+roleName+")") // Log with role names
roleNames := make([]string, len(user.Roles))
for i, role := range user.Roles {
roleNames[i] = role.Name
}
logging.InfoOperation("USER_CREATE", "Created user: "+user.Email+" (ID: "+user.ID+", Roles: "+strings.Join(roleNames, ", ")+")")
return user, nil return user, nil
} }
@@ -105,13 +110,6 @@ func (s *MembershipService) GetUserWithPermissions(ctx context.Context, userID s
return s.repo.FindUserByIDWithPermissions(ctx, userID) return s.repo.FindUserByIDWithPermissions(ctx, userID)
} }
// UpdateUserRequest defines the request body for updating a user.
type UpdateUserRequest struct {
Username *string `json:"username"`
Password *string `json:"password"`
RoleID *string `json:"roleId"`
}
// DeleteUser deletes a user with validation to prevent Super Admin deletion. // DeleteUser deletes a user with validation to prevent Super Admin deletion.
func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) error { func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) error {
// Get user with role information // Get user with role information
@@ -142,34 +140,42 @@ func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) er
} }
// UpdateUser updates a user's details. // UpdateUser updates a user's details.
func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, req UpdateUserRequest) (*model.User, error) { func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, req *model.UserUpdateRequest) (*model.User, error) {
// Validate request
if err := req.Validate(); err != nil {
return nil, err
}
user, err := s.repo.FindUserByID(ctx, userID) user, err := s.repo.FindUserByID(ctx, userID)
if err != nil { if err != nil {
return nil, errors.New("user not found") return nil, errors.New("user not found")
} }
if req.Username != nil { // Apply update request to user
user.Username = *req.Username if err := req.ApplyToUser(user); err != nil {
return nil, err
} }
if req.Password != nil && *req.Password != "" { // Handle roles if provided
// Use the model's SetPassword method to hash the password if len(req.RoleIDs) > 0 {
if err := user.SetPassword(*req.Password); err != nil { roles := make([]model.Role, 0, len(req.RoleIDs))
return nil, err for _, roleID := range req.RoleIDs {
roleUUID, err := uuid.Parse(roleID)
if err != nil {
return nil, errors.New("invalid role ID format: " + roleID)
}
role, err := s.repo.FindRoleByID(ctx, roleUUID)
if err != nil {
return nil, errors.New("role not found: " + roleID)
}
roles = append(roles, *role)
} }
user.Roles = roles
} }
if req.RoleID != nil { // Validate updated user
// Check if role exists if err := user.Validate(); err != nil {
roleUUID, err := uuid.Parse(*req.RoleID) return nil, err
if err != nil {
return nil, errors.New("invalid role ID format")
}
role, err := s.repo.FindRoleByID(ctx, roleUUID)
if err != nil {
return nil, errors.New("role not found")
}
user.Roles = []model.Role{*role}
} }
if err := s.repo.UpdateUser(ctx, user); err != nil { if err := s.repo.UpdateUser(ctx, user); err != nil {
@@ -177,11 +183,11 @@ func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, re
} }
// Invalidate cache if role was changed // Invalidate cache if role was changed
if req.RoleID != nil && s.cacheInvalidator != nil { if len(req.RoleIDs) > 0 && s.cacheInvalidator != nil {
s.cacheInvalidator.InvalidateUserPermissions(userID.String()) s.cacheInvalidator.InvalidateUserPermissions(userID.String())
} }
logging.InfoOperation("USER_UPDATE", "Updated user: "+user.Username+" (ID: "+user.ID+")") logging.InfoOperation("USER_UPDATE", "Updated user: "+user.Email+" (ID: "+user.ID+")")
return user, nil return user, nil
} }
@@ -289,10 +295,17 @@ func (s *MembershipService) SetupInitialData(ctx context.Context) error {
} }
// Create a default admin user if one doesn't exist // Create a default admin user if one doesn't exist
_, err = s.repo.FindUserByUsername(ctx, "admin") _, err = s.repo.FindUserByEmail(ctx, "admin@example.com")
if err != nil { if err != nil {
logging.Debug("Creating default admin user") logging.Debug("Creating default admin user")
_, err = s.CreateUser(ctx, "admin", os.Getenv("PASSWORD"), "Super Admin") // Default password, should be changed adminUser := &model.User{
Email: "admin@example.com",
FullName: "System Administrator",
}
if err := adminUser.SetPassword(os.Getenv("PASSWORD")); err != nil {
return err
}
_, err = s.CreateUser(ctx, adminUser, []string{"Super Admin"})
if err != nil { if err != nil {
return err return err
} }

View File

@@ -1,28 +0,0 @@
package service
import (
"context"
"omega-server/local/model"
"github.com/google/uuid"
)
// MembershipServiceInterface defines the interface for membership-related operations
type MembershipServiceInterface interface {
// Authentication and Authorization
Login(ctx context.Context, username, password string) (string, error)
HasPermission(ctx context.Context, userID string, permissionName string) (bool, error)
GetUserWithPermissions(ctx context.Context, userID string) (*model.User, error)
SetCacheInvalidator(invalidator CacheInvalidator)
// User Management
CreateUser(ctx context.Context, username, password, roleName string) (*model.User, error)
ListUsers(ctx context.Context) ([]*model.User, error)
GetUser(ctx context.Context, userID uuid.UUID) (*model.User, error)
DeleteUser(ctx context.Context, userID uuid.UUID) error
UpdateUser(ctx context.Context, userID uuid.UUID, req UpdateUserRequest) (*model.User, error)
// Role Management
GetAllRoles(ctx context.Context) ([]*model.Role, error)
SetupInitialData(ctx context.Context) error
}

View File

@@ -107,7 +107,7 @@ func DefaultPagination() PaginationRequest {
return PaginationRequest{ return PaginationRequest{
Page: 1, Page: 1,
Limit: 10, Limit: 10,
Sort: "dateCreated", Sort: "created_at",
Order: "desc", Order: "desc",
} }
} }
@@ -121,7 +121,7 @@ func (p *PaginationRequest) Validate() {
p.Limit = 10 p.Limit = 10
} }
if p.Sort == "" { if p.Sort == "" {
p.Sort = "dateCreated" p.Sort = "created_at"
} }
if p.Order != "asc" && p.Order != "desc" { if p.Order != "asc" && p.Order != "desc" {
p.Order = "desc" p.Order = "desc"

View File

@@ -1,23 +1,53 @@
package db package db
import ( import (
"fmt"
"omega-server/local/model" "omega-server/local/model"
"omega-server/local/utl/logging" "omega-server/local/utl/logging"
"os" "os"
"time" "time"
"go.uber.org/dig" "go.uber.org/dig"
"gorm.io/driver/sqlite" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
) )
func Start(di *dig.Container) { func Start(di *dig.Container) {
// PostgreSQL connection configuration
host := os.Getenv("DB_HOST")
if host == "" {
host = "localhost"
}
port := os.Getenv("DB_PORT")
if port == "" {
port = "5432"
}
user := os.Getenv("DB_USER")
if user == "" {
user = "postgres"
}
password := os.Getenv("DB_PASSWORD")
if password == "" {
password = "password"
}
dbName := os.Getenv("DB_NAME") dbName := os.Getenv("DB_NAME")
if dbName == "" { if dbName == "" {
dbName = "app.db" dbName = "omega_db"
} }
sslMode := os.Getenv("DB_SSL_MODE")
if sslMode == "" {
sslMode = "disable"
}
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=UTC",
host, user, password, dbName, port, sslMode)
// Configure GORM logger // Configure GORM logger
gormLogger := logger.Default gormLogger := logger.Default
if os.Getenv("LOG_LEVEL") == "DEBUG" { if os.Getenv("LOG_LEVEL") == "DEBUG" {
@@ -26,10 +56,14 @@ func Start(di *dig.Container) {
gormLogger = logger.Default.LogMode(logger.Silent) gormLogger = logger.Default.LogMode(logger.Silent)
} }
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: gormLogger, Logger: gormLogger,
}) })
if err != nil { if err != nil {
logging.Error("Failed to connect to PostgreSQL database")
logging.Error("Connection string: host=%s user=%s dbname=%s port=%s sslmode=%s", host, user, dbName, port, sslMode)
logging.Error("Error: %v", err)
logging.Error("Make sure PostgreSQL is running and the database exists")
logging.Panic("failed to connect database: " + err.Error()) logging.Panic("failed to connect database: " + err.Error())
} }
@@ -62,6 +96,12 @@ func Migrate(db *gorm.DB) {
&model.User{}, &model.User{},
&model.Role{}, &model.Role{},
&model.Permission{}, &model.Permission{},
&model.Type{},
&model.Project{},
&model.Task{},
&model.Integration{},
&model.ProjectMember{},
&model.TaskAssignee{},
&model.SystemConfig{}, &model.SystemConfig{},
&model.AuditLog{}, &model.AuditLog{},
&model.SecurityEvent{}, &model.SecurityEvent{},
@@ -87,6 +127,9 @@ func Seed(db *gorm.DB) error {
if err := seedDefaultAdmin(db); err != nil { if err := seedDefaultAdmin(db); err != nil {
return err return err
} }
if err := seedDefaultTypes(db); err != nil {
return err
}
if err := seedSystemConfigs(db); err != nil { if err := seedSystemConfigs(db); err != nil {
return err return err
} }
@@ -193,6 +236,31 @@ func seedPermissions(db *gorm.DB) error {
return nil return nil
} }
func seedDefaultTypes(db *gorm.DB) error {
defaultTypes := []model.Type{
{Name: "Web Development", Description: "Standard web development project"},
{Name: "Mobile App", Description: "Mobile application development"},
{Name: "API Development", Description: "API and backend service development"},
{Name: "Data Science", Description: "Data analysis and machine learning projects"},
{Name: "DevOps", Description: "Infrastructure and deployment projects"},
{Name: "Research", Description: "Research and documentation projects"},
}
for _, projectType := range defaultTypes {
var existingType model.Type
err := db.Where("name = ? AND user_id IS NULL", projectType.Name).First(&existingType).Error
if err == gorm.ErrRecordNotFound {
projectType.Init()
if err := db.Create(&projectType).Error; err != nil {
return err
}
logging.Info("Created default project type: %s", projectType.Name)
}
}
return nil
}
func seedDefaultAdmin(db *gorm.DB) error { func seedDefaultAdmin(db *gorm.DB) error {
// Check if admin user already exists // Check if admin user already exists
var existingAdmin model.User var existingAdmin model.User
@@ -214,10 +282,9 @@ func seedDefaultAdmin(db *gorm.DB) error {
} }
admin := model.User{ admin := model.User{
Email: "admin@example.com", Email: "admin@example.com",
Username: "admin", FullName: "System Administrator",
Name: "System Administrator", PasswordHash: "",
Active: true,
} }
admin.Init() admin.Init()