implement graphQL and init postgres
This commit is contained in:
13
go.mod
13
go.mod
@@ -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
33
go.sum
@@ -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=
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
421
local/graphql/handler/handler.go
Normal file
421
local/graphql/handler/handler.go
Normal 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!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
100
local/graphql/schema/schema.graphql
Normal file
100
local/graphql/schema/schema.graphql
Normal 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!
|
||||||
|
}
|
||||||
173
local/graphql/service/service.go
Normal file
173
local/graphql/service/service.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
144
local/model/integration.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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":
|
||||||
|
|||||||
@@ -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
148
local/model/project.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
213
local/model/task.go
Normal 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
95
local/model/type.go
Normal 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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user