init project
|
@ -0,0 +1,7 @@
|
||||||
|
git add *
|
||||||
|
|
||||||
|
read -p "Commit message: " commit_message
|
||||||
|
|
||||||
|
git commit -m "$commit_message"
|
||||||
|
|
||||||
|
git push -u origin main
|
|
@ -0,0 +1,40 @@
|
||||||
|
module lms.de/backend
|
||||||
|
|
||||||
|
go 1.21.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.ex.umbach.dev/Alex/roese-utils v1.0.21
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.5
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/rs/zerolog v1.31.0
|
||||||
|
golang.org/x/crypto v0.14.0
|
||||||
|
gorm.io/driver/mysql v1.5.7
|
||||||
|
gorm.io/gorm v1.25.11
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||||
|
github.com/fasthttp/websocket v1.5.3 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.15.5 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
|
github.com/gofiber/websocket/v2 v2.2.1 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
golang.org/x/net v0.17.0 // indirect
|
||||||
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
|
golang.org/x/text v0.17.0 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,90 @@
|
||||||
|
git.ex.umbach.dev/Alex/roese-utils v1.0.21 h1:ae1AHQh8UHJVLbpk5Gf4S5IP0EEWGD1JFIeX9cIcYcc=
|
||||||
|
git.ex.umbach.dev/Alex/roese-utils v1.0.21/go.mod h1:hFcnKQl6nuGFEMCxK/eVQBUD6ixBFlAaiy4E2aQqUL8=
|
||||||
|
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||||
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
|
||||||
|
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24=
|
||||||
|
github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||||
|
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
|
||||||
|
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
|
||||||
|
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||||
|
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
|
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
|
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||||
|
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||||
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||||
|
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||||
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
|
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
|
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||||
|
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||||
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
|
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||||
|
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
|
@ -0,0 +1,111 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.ex.umbach.dev/Alex/roese-utils/rsconfig"
|
||||||
|
"git.ex.umbach.dev/Alex/roese-utils/rslogger"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
"github.com/gofiber/websocket/v2"
|
||||||
|
"lms.de/backend/modules/config"
|
||||||
|
"lms.de/backend/modules/database"
|
||||||
|
"lms.de/backend/modules/logger"
|
||||||
|
"lms.de/backend/modules/structs"
|
||||||
|
"lms.de/backend/modules/utils"
|
||||||
|
"lms.de/backend/routers/router"
|
||||||
|
"lms.de/backend/socketserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
fmt.Println("Server is starting...")
|
||||||
|
|
||||||
|
rsconfig.CreateEnvConfigFileIfNotExists(`DEBUG=false
|
||||||
|
COLORIZED_OUTPUT=true
|
||||||
|
HOST=127.0.0.1
|
||||||
|
PORT=8080
|
||||||
|
|
||||||
|
LOG_MANAGER_SERVER_URL=http://localhost:50110
|
||||||
|
|
||||||
|
# Folder paths
|
||||||
|
FOLDER_PUBLIC_STATIC=./public/
|
||||||
|
|
||||||
|
# MariaDB
|
||||||
|
MARIADB_HOSTNAME=127.0.0.1
|
||||||
|
MARIADB_PORT=3306
|
||||||
|
MARIADB_USERNAME=db_user
|
||||||
|
MARIADB_PASSWORD=db_password
|
||||||
|
MARIADB_DATABASE_NAME=db_database_name`)
|
||||||
|
|
||||||
|
config.LoadConfig()
|
||||||
|
|
||||||
|
rslogger.InitLogger(config.Cfg.Debug, config.Cfg.ColorizedOutput, config.Cfg.LogManagerServerUrl)
|
||||||
|
|
||||||
|
if os.Getenv("DOCKER") != "" {
|
||||||
|
fmt.Println("Waiting for mariadb docker")
|
||||||
|
// waiting for the start of mariadb docker
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ValidatorInit()
|
||||||
|
database.InitDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := fiber.New(fiber.Config{
|
||||||
|
BodyLimit: 100 * 1024 * 1024,
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Use(cors.New())
|
||||||
|
|
||||||
|
router.SetupRoutes(app)
|
||||||
|
|
||||||
|
app.Use("/ws", func(c *fiber.Ctx) error {
|
||||||
|
// IsWebSocketUpgrade returns true if the client
|
||||||
|
// requested upgrade to the WebSocket protocol.
|
||||||
|
if websocket.IsWebSocketUpgrade(c) {
|
||||||
|
sessionId := c.Query("auth")
|
||||||
|
// needed for a user who uses multiple tabs in the browser
|
||||||
|
// with the same session id because otherwise the last browser
|
||||||
|
// tab would subscribe to the topic and the other tabs would
|
||||||
|
// not receive any messages
|
||||||
|
browserTabSession := c.Query("bts")
|
||||||
|
|
||||||
|
if len(sessionId) != utils.LenHeaderXAuthorization ||
|
||||||
|
len(browserTabSession) != utils.LenHeaderXAuthorization {
|
||||||
|
return c.SendStatus(fiber.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate ws session
|
||||||
|
var userSession structs.UserSession
|
||||||
|
|
||||||
|
database.DB.Select("user_id").First(&userSession, "session = ?", sessionId)
|
||||||
|
|
||||||
|
if userSession.UserId != "" {
|
||||||
|
var user structs.User
|
||||||
|
|
||||||
|
database.DB.First(&user, "id = ?", userSession.UserId)
|
||||||
|
|
||||||
|
if user.Id != "" {
|
||||||
|
c.Locals("sessionId", sessionId)
|
||||||
|
c.Locals("browserTabSession", browserTabSession)
|
||||||
|
c.Locals("userId", user.Id)
|
||||||
|
c.Locals("organizationId", user.OrganizationId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fiber.ErrUpgradeRequired
|
||||||
|
})
|
||||||
|
|
||||||
|
go socketserver.RunHub()
|
||||||
|
socketserver.WebSocketServer(app)
|
||||||
|
|
||||||
|
logger.AddSystemLog(rslogger.LogTypeInfo, "Server started")
|
||||||
|
|
||||||
|
app.Listen(config.Cfg.Host + ":" + config.Cfg.Port)
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gofiber/websocket/v2"
|
||||||
|
"lms.de/backend/modules/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var socketClients []*structs.SocketClient
|
||||||
|
var mu sync.RWMutex
|
||||||
|
|
||||||
|
func AddSocketClient(socketClient *structs.SocketClient) {
|
||||||
|
mu.Lock()
|
||||||
|
socketClients = append(socketClients, socketClient)
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteClientByConn(conn *websocket.Conn) {
|
||||||
|
mu.Lock()
|
||||||
|
|
||||||
|
for i := 0; i < len(socketClients); i++ {
|
||||||
|
if socketClients[i].Conn == conn {
|
||||||
|
socketClients = removeSocketClient(socketClients, i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeSocketClient(s []*structs.SocketClient, i int) []*structs.SocketClient {
|
||||||
|
return append(s[:i], s[i+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSocketClients() []*structs.SocketClient {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
|
||||||
|
return socketClients
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubscribeSocketClientToTopic(browserTabSession string, topic string) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
for _, socketClient := range socketClients {
|
||||||
|
if socketClient.BrowserTabSession == browserTabSession {
|
||||||
|
socketClient.SubscribedTopic = topic
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Cfg Config
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Debug bool
|
||||||
|
ColorizedOutput bool
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
LogManagerServerUrl string
|
||||||
|
FolderPaths FolderPaths
|
||||||
|
MariaDB MariaDB
|
||||||
|
}
|
||||||
|
|
||||||
|
type FolderPaths struct {
|
||||||
|
PublicStatic string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MariaDB struct {
|
||||||
|
Hostname string
|
||||||
|
Port string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
DatabaseName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig() {
|
||||||
|
// used to determine server was startet in docker or not
|
||||||
|
if os.Getenv("DOCKER") == "" {
|
||||||
|
fmt.Println("Load env from file")
|
||||||
|
godotenv.Load(".env")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Load env from system")
|
||||||
|
}
|
||||||
|
|
||||||
|
config := Config{
|
||||||
|
Debug: os.Getenv("DEBUG") == "true",
|
||||||
|
ColorizedOutput: os.Getenv("COLORIZED_OUTPUT") == "true",
|
||||||
|
LogManagerServerUrl: os.Getenv("LOG_MANAGER_SERVER_URL"),
|
||||||
|
FolderPaths: FolderPaths{
|
||||||
|
PublicStatic: os.Getenv("FOLDER_PUBLIC_STATIC"),
|
||||||
|
},
|
||||||
|
MariaDB: MariaDB{
|
||||||
|
Hostname: os.Getenv("MARIADB_HOSTNAME"),
|
||||||
|
Port: os.Getenv("MARIADB_PORT"),
|
||||||
|
Username: os.Getenv("MARIADB_USERNAME"),
|
||||||
|
Password: os.Getenv("MARIADB_PASSWORD"),
|
||||||
|
DatabaseName: os.Getenv("MARIADB_DATABASE_NAME"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// load default values if not in docker
|
||||||
|
if os.Getenv("DOCKER") == "" {
|
||||||
|
config.Host = os.Getenv("HOST")
|
||||||
|
config.Port = os.Getenv("PORT")
|
||||||
|
} else { // load from docker env
|
||||||
|
config.Host = "0.0.0.0"
|
||||||
|
config.Port = "80"
|
||||||
|
}
|
||||||
|
|
||||||
|
Cfg = config
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
"lms.de/backend/modules/config"
|
||||||
|
"lms.de/backend/modules/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DB *gorm.DB
|
||||||
|
|
||||||
|
func InitDatabase() {
|
||||||
|
cfg := config.Cfg
|
||||||
|
|
||||||
|
var logMode logger.LogLevel
|
||||||
|
|
||||||
|
if cfg.Debug {
|
||||||
|
logMode = logger.Error
|
||||||
|
} else {
|
||||||
|
logMode = logger.Silent
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := gorm.Open(mysql.Open(
|
||||||
|
fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", cfg.MariaDB.Username, cfg.MariaDB.Password, cfg.MariaDB.Hostname, cfg.MariaDB.Port, cfg.MariaDB.DatabaseName)),
|
||||||
|
&gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logMode),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DB = db
|
||||||
|
|
||||||
|
db.AutoMigrate(&structs.Organization{})
|
||||||
|
db.AutoMigrate(&structs.Role{})
|
||||||
|
db.AutoMigrate(&structs.RolePermission{})
|
||||||
|
db.AutoMigrate(&structs.User{})
|
||||||
|
db.AutoMigrate(&structs.UserSession{})
|
||||||
|
db.AutoMigrate(&structs.Lesson{})
|
||||||
|
db.AutoMigrate(&structs.LessonContent{})
|
||||||
|
db.AutoMigrate(&structs.Question{})
|
||||||
|
db.AutoMigrate(&structs.QuestionLike{})
|
||||||
|
db.AutoMigrate(&structs.QuestionReply{})
|
||||||
|
db.AutoMigrate(&structs.QuestionReplyLike{})
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.ex.umbach.dev/Alex/roese-utils/rslogger"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddSystemLog(logType string, format string, v ...any) {
|
||||||
|
go rslogger.LogManagerRequestClient(fiber.MethodPost, rslogger.LogManagerRequestBody{
|
||||||
|
Type: "lms-system",
|
||||||
|
Logs: []string{logType + rslogger.GetTime() + fmt.Sprintf(format, v...)}})
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package structs
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
LessonStateActive = 1
|
||||||
|
LessonStateDraft = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type Lesson struct {
|
||||||
|
Id string `gorm:"primaryKey;type:varchar(36)"`
|
||||||
|
OrganizationId string `gorm:"type:varchar(36)"`
|
||||||
|
State uint8 `gorm:"type:tinyint(1)"`
|
||||||
|
Title string `gorm:"type:varchar(255)"`
|
||||||
|
ThumbnailUrl string `gorm:"type:varchar(255)"`
|
||||||
|
CreatorUserId string `gorm:"type:varchar(36)"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type LessonContent struct {
|
||||||
|
Id string `gorm:"primaryKey;type:varchar(36)"`
|
||||||
|
LessonId string `gorm:"type:varchar(36)"`
|
||||||
|
Page uint16 `gorm:"type:smallint(5)"` // Page number
|
||||||
|
Position uint16 `gorm:"type:smallint(5)"` // Position on the page
|
||||||
|
Type uint8 // Type of content, like text, image, video, etc
|
||||||
|
Data string `gorm:"type:text"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model LessonResponse
|
||||||
|
type LessonResponse struct {
|
||||||
|
Id string
|
||||||
|
State uint8
|
||||||
|
Title string
|
||||||
|
ThumbnailUrl string
|
||||||
|
CreatorUserId string
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model CreateLessonResponse
|
||||||
|
type CreateLessonResponse struct {
|
||||||
|
Id string
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model LessonSettings
|
||||||
|
type LessonSettings struct {
|
||||||
|
Title string
|
||||||
|
ThumbnailUrl string
|
||||||
|
State uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model UpdateLessonPreviewTitleRequest
|
||||||
|
type UpdateLessonPreviewTitleRequest struct {
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model UpdateLessonStateRequest
|
||||||
|
type UpdateLessonStateRequest struct {
|
||||||
|
State uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model AddLessonContentRequest
|
||||||
|
type AddLessonContentRequest struct {
|
||||||
|
Type uint8
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model AddLessonContentResponse
|
||||||
|
type AddLessonContentResponse struct {
|
||||||
|
Id string
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model UpdateLessonContentRequest
|
||||||
|
type UpdateLessonContentRequest struct {
|
||||||
|
Type uint8
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model UpdateLessonContentResponse
|
||||||
|
type LessonIdParam struct {
|
||||||
|
LessonId string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LessonIdAndContentIdParam struct {
|
||||||
|
LessonId string
|
||||||
|
ContentId string
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model GetLessonContentsResponse
|
||||||
|
type GetLessonContentsResponse struct {
|
||||||
|
Id string
|
||||||
|
Page uint16
|
||||||
|
Position uint16
|
||||||
|
Type uint8
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateLessonContentPositionRequest struct {
|
||||||
|
Position uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadLessonContentFileParam struct {
|
||||||
|
LessonId string
|
||||||
|
ContentId string
|
||||||
|
Type string
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package structs
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Organization struct {
|
||||||
|
Id string `gorm:"primaryKey;type:varchar(36)"`
|
||||||
|
Subdomain string `gorm:"type:varchar(255)"`
|
||||||
|
OwnerUserId string `gorm:"type:varchar(36)"`
|
||||||
|
CompanyName string `gorm:"type:varchar(255)"`
|
||||||
|
PrimaryColor string `gorm:"type:varchar(6)"`
|
||||||
|
LogoUrl string `gorm:"type:varchar(255)"`
|
||||||
|
BannerUrl string `gorm:"type:varchar(255)"`
|
||||||
|
SignUpScreenUrl string `gorm:"type:varchar(255)"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model CreateOrganizationRequest
|
||||||
|
type CreateOrganizationRequest struct {
|
||||||
|
Email string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model CreateOrganizationResponse
|
||||||
|
type CreateOrganizationResponse struct {
|
||||||
|
OrganizationSubdomain string
|
||||||
|
Session string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetOrganizationSettingsResponse struct {
|
||||||
|
Subdomain string
|
||||||
|
CompanyName string
|
||||||
|
PrimaryColor string
|
||||||
|
LogoUrl string
|
||||||
|
BannerUrl string
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package structs
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Question struct {
|
||||||
|
Id string `gorm:"primaryKey;type:varchar(36)"`
|
||||||
|
LessonId string `gorm:"type:varchar(36)"`
|
||||||
|
Question string `gorm:"type:text"`
|
||||||
|
Likes uint16 `gorm:"type:smallint(5)"`
|
||||||
|
CreatorUserId string `gorm:"type:varchar(36)"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuestionLike struct {
|
||||||
|
Id string `gorm:"primaryKey;type:varchar(36)"`
|
||||||
|
QuestionId string `gorm:"type:varchar(36)"`
|
||||||
|
CreatorUserId string `gorm:"type:varchar(36)"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuestionReply struct {
|
||||||
|
Id string `gorm:"primaryKey;type:varchar(36)"`
|
||||||
|
QuestionId string `gorm:"type:varchar(36)"`
|
||||||
|
Reply string `gorm:"type:text"`
|
||||||
|
CreatorUserId string `gorm:"type:varchar(36)"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuestionReplyLike struct {
|
||||||
|
Id string `gorm:"primaryKey;type:varchar(36)"`
|
||||||
|
QuestionReplyId string `gorm:"type:varchar(36)"`
|
||||||
|
CreatorUserId string `gorm:"type:varchar(36)"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package structs
|
||||||
|
|
||||||
|
type Role struct {
|
||||||
|
Id string `gorm:"primaryKey;type:varchar(36)"`
|
||||||
|
OrganizationId string `gorm:"type:varchar(36)"`
|
||||||
|
Name string `gorm:"type:varchar(255)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RolePermission struct {
|
||||||
|
Id string `gorm:"primaryKey;type:varchar(36)"`
|
||||||
|
RoleId string `gorm:"type:varchar(36)"`
|
||||||
|
Permission string `gorm:"type:varchar(255)"`
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
package structs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/websocket/v2"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Note: Status codes in the range 4000-4999 are reserved for private use
|
||||||
|
SocketCloseCodeUnauthorized = 4001
|
||||||
|
SocketCloseCodeSessionClosed = 4002 // when user logs out of the session and has multiple tabs with the same session open
|
||||||
|
)
|
||||||
|
|
||||||
|
type SocketClient struct {
|
||||||
|
SessionId string
|
||||||
|
BrowserTabSession string
|
||||||
|
UserId string
|
||||||
|
Conn *websocket.Conn
|
||||||
|
connMu sync.Mutex
|
||||||
|
SubscribedTopic string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SocketMessage struct {
|
||||||
|
Conn *websocket.Conn
|
||||||
|
Msg []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendSocketMessage struct {
|
||||||
|
Cmd int
|
||||||
|
Body any
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReceivedMessage struct {
|
||||||
|
Cmd int
|
||||||
|
Body map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (socketClient *SocketClient) SendSessionClosedMessage() error {
|
||||||
|
return socketClient.writeMessage(websocket.CloseMessage, SendSocketMessage{}, SocketCloseCodeSessionClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (socketClient *SocketClient) SendUnauthorizedCloseMessage() error {
|
||||||
|
return socketClient.writeMessage(websocket.CloseMessage, SendSocketMessage{}, SocketCloseCodeUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (socketClient *SocketClient) SendMessage(message SendSocketMessage) error {
|
||||||
|
return socketClient.writeMessage(websocket.TextMessage, message, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (socketClient *SocketClient) writeMessage(messageType int, message SendSocketMessage, closeMessageCode int) error {
|
||||||
|
var marshaledMessage []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if closeMessageCode > 0 {
|
||||||
|
marshaledMessage = websocket.FormatCloseMessage(closeMessageCode, "")
|
||||||
|
} else {
|
||||||
|
marshaledMessage, err = json.Marshal(message)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("Failed to marshal ws message, err: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socketClient.connMu.Lock()
|
||||||
|
defer socketClient.connMu.Unlock()
|
||||||
|
|
||||||
|
if socketClient.Conn == nil {
|
||||||
|
log.Error().Msgf("Failed to ws message because conn is nil")
|
||||||
|
return errors.New("ws client conn is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = socketClient.Conn.WriteMessage(messageType, marshaledMessage)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("Failed to write ws message, err: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AllUsers struct {
|
||||||
|
Id string
|
||||||
|
RoleId string
|
||||||
|
Avatar string
|
||||||
|
Username string
|
||||||
|
ConnectionStatus uint8
|
||||||
|
Deactivated bool
|
||||||
|
LastOnline time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserSessionSocket struct {
|
||||||
|
IdForDeletion string
|
||||||
|
UserAgent string
|
||||||
|
ConnectionStatus uint8
|
||||||
|
LastUsed time.Time
|
||||||
|
ExpiresAt time.Time
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package structs
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Id string `gorm:"primaryKey;type:varchar(36)"`
|
||||||
|
OrganizationId string `gorm:"type:varchar(36)"`
|
||||||
|
State uint8 `gorm:"type:tinyint(1)"`
|
||||||
|
Active bool `gorm:"type:tinyint(1)"`
|
||||||
|
RoleId string `gorm:"type:varchar(36)"`
|
||||||
|
FirstName string `gorm:"type:varchar(255)"`
|
||||||
|
LastName string `gorm:"type:varchar(255)"`
|
||||||
|
Email string `gorm:"type:varchar(255)"`
|
||||||
|
Password string `gorm:"type:varchar(255)"`
|
||||||
|
ProfilePictureUrl string `gorm:"type:varchar(255)"`
|
||||||
|
LastOnlineAt time.Time
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserSession struct {
|
||||||
|
Id string `gorm:"primaryKey;type:varchar(36)"`
|
||||||
|
UserId string `gorm:"type:varchar(36)"`
|
||||||
|
OrganizationId string `gorm:"type:varchar(36)"`
|
||||||
|
Session string `gorm:"type:varchar(36)"`
|
||||||
|
UserAgent string `gorm:"type:varchar(255)"`
|
||||||
|
ExpiresAt time.Time
|
||||||
|
LastUsedAt time.Time
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUserResponse struct {
|
||||||
|
AvatarUrl string
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model UserLoginRequest
|
||||||
|
type UserLoginRequest struct {
|
||||||
|
Email string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model UserLoginResponse
|
||||||
|
type UserLoginResponse struct {
|
||||||
|
Session string
|
||||||
|
}
|
||||||
|
|
||||||
|
// swagger:model TeamMember
|
||||||
|
type TeamMember struct {
|
||||||
|
Id string
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
Email string
|
||||||
|
RoleId string
|
||||||
|
ProfilePictureUrl string
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
const (
|
||||||
|
minPassword = "6"
|
||||||
|
MinPassword = 6
|
||||||
|
maxPassword = "64"
|
||||||
|
MaxPassword = 64
|
||||||
|
|
||||||
|
LenHeaderXAuthorization = 36
|
||||||
|
lenHeaderXAuthorization = "36"
|
||||||
|
LenUserId = 36
|
||||||
|
LenHeaderXApiKey = 36
|
||||||
|
|
||||||
|
HeaderXAuthorization = "X-Authorization"
|
||||||
|
HeaderXApiKey = "X-Api-Key"
|
||||||
|
|
||||||
|
SessionExpiresAtTime = 7 * 24 * 60 * 60 // 1 week
|
||||||
|
|
||||||
|
MaxImageSize = 25 * 1024 * 1024 // 25MB
|
||||||
|
MaxVideoSize = 50 * 1024 * 1024 // 50MB
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
AcceptedImageFileTypes = []string{
|
||||||
|
"image/png",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/jpg",
|
||||||
|
"image/webp"}
|
||||||
|
|
||||||
|
AcceptedVideoFileTypes = []string{
|
||||||
|
"video/mp4",
|
||||||
|
"video/webm",
|
||||||
|
"video/mkv"}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LessonContentTypeHeader uint8 = 1
|
||||||
|
LessonContentTypeText = 2
|
||||||
|
LessonContentTypeImage
|
||||||
|
)
|
|
@ -0,0 +1,130 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"lms.de/backend/modules/config"
|
||||||
|
"lms.de/backend/modules/database"
|
||||||
|
"lms.de/backend/modules/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetXAuhorizationHeader(c *fiber.Ctx) string {
|
||||||
|
// check if header is set
|
||||||
|
if len(c.GetReqHeaders()[HeaderXAuthorization]) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.GetReqHeaders()[HeaderXAuthorization][0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetXApiKeyHeader(c *fiber.Ctx) string {
|
||||||
|
// check if header is set
|
||||||
|
if len(c.GetReqHeaders()[HeaderXApiKey]) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.GetReqHeaders()[HeaderXApiKey][0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSessionExpiresAtTime() time.Time {
|
||||||
|
return time.Now().Add(time.Second * SessionExpiresAtTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsPasswordLengthValid(password string) bool {
|
||||||
|
lenPassword := len(password)
|
||||||
|
|
||||||
|
if lenPassword < MinPassword || lenPassword > MaxPassword {
|
||||||
|
log.Error().Msg("Password length not valid")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// generates a random subdomain and check if it is already in use
|
||||||
|
func GenerateSubdomain() string {
|
||||||
|
var subdomain string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for {
|
||||||
|
subdomain, err = GenerateCode(6)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msg("Failed to generate subdomain")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsSubdomainAlreadyInUse(subdomain) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msg("Subdomain already in use, generating new one")
|
||||||
|
}
|
||||||
|
|
||||||
|
return subdomain
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsSubdomainAlreadyInUse(subdomain string) bool {
|
||||||
|
var organization structs.Organization
|
||||||
|
|
||||||
|
if err := database.DB.Where("subdomain = ?", subdomain).First(&organization).Error; err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateCode(codeLength int) (string, error) {
|
||||||
|
var letters = "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
|
r := make([]byte, codeLength)
|
||||||
|
|
||||||
|
for i := 0; i < codeLength; i++ {
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("Failed to session: %v", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
r[i] = letters[num.Int64()]
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(r), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsFileTypeAllowed(contentType string, allowedContentTypes []string) bool {
|
||||||
|
for _, aType := range allowedContentTypes {
|
||||||
|
if aType == contentType {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteFile(filePath string) {
|
||||||
|
os.Remove(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateFolderStructureIfNotExists(folderPath string) {
|
||||||
|
if _, err := os.Stat(folderPath); os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(folderPath, os.ModePerm); err != nil {
|
||||||
|
log.Error().Msgf("Failed to create folder structure, err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFullImagePath returns the database path and the public path for the image
|
||||||
|
func GetFullImagePath(organizationId string, lessonId string) (databasePath string, publicPath string) {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"o/%s/l/%s/", organizationId, lessonId),
|
||||||
|
fmt.Sprintf(
|
||||||
|
"%s/o/%s/l/%s/", config.Cfg.FolderPaths.PublicStatic, organizationId, lessonId)
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
func ValidatorInit() {
|
||||||
|
|
||||||
|
}
|
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 308 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 102 KiB |
After Width: | Height: | Size: 232 KiB |
After Width: | Height: | Size: 526 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 340 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 182 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 308 KiB |
After Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 297 KiB |
After Width: | Height: | Size: 308 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 1.4 MiB |
After Width: | Height: | Size: 5.2 MiB |
After Width: | Height: | Size: 382 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 256 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 116 KiB |
After Width: | Height: | Size: 181 KiB |
After Width: | Height: | Size: 405 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 308 KiB |
After Width: | Height: | Size: 236 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 478 B |
After Width: | Height: | Size: 160 KiB |
After Width: | Height: | Size: 309 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 264 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 233 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 249 KiB |
After Width: | Height: | Size: 240 KiB |
After Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 124 KiB |
|
@ -0,0 +1,673 @@
|
||||||
|
package lessons
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"lms.de/backend/modules/database"
|
||||||
|
"lms.de/backend/modules/structs"
|
||||||
|
"lms.de/backend/modules/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetLessons(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation GET /v1/lessons lessons GetLessons
|
||||||
|
// ---
|
||||||
|
// summary: Get lessons.
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Lessons retrieved successfully.
|
||||||
|
// schema:
|
||||||
|
// type: array
|
||||||
|
// items:
|
||||||
|
// "$ref": "#/definitions/LessonResponse"
|
||||||
|
// '500':
|
||||||
|
// description: Failed to retrieve lessons.
|
||||||
|
|
||||||
|
var lessons []structs.LessonResponse
|
||||||
|
|
||||||
|
database.DB.Model(&structs.Lesson{}).Where("organization_id = ?", c.Locals("organizationId")).Find(&lessons)
|
||||||
|
|
||||||
|
return c.JSON(lessons)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateLesson(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation POST /v1/lessons lessons CreateLesson
|
||||||
|
// ---
|
||||||
|
// summary: Create lesson.
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Lesson created successfully.
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/CreateLessonResponse"
|
||||||
|
// '500':
|
||||||
|
// description: Failed to create lesson.
|
||||||
|
|
||||||
|
lesson := structs.Lesson{
|
||||||
|
Id: uuid.New().String(),
|
||||||
|
OrganizationId: c.Locals("organizationId").(string),
|
||||||
|
State: structs.LessonStateDraft,
|
||||||
|
Title: "Test",
|
||||||
|
CreatorUserId: c.Locals("userId").(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
database.DB.Create(&lesson)
|
||||||
|
|
||||||
|
return c.JSON(
|
||||||
|
structs.CreateLessonResponse{
|
||||||
|
Id: lesson.Id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLessonContents(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation GET /v1/lessons/{lessonId}/contents lessons GetLessonContents
|
||||||
|
// ---
|
||||||
|
// summary: Get lesson contents.
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: lessonId
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Lesson contents retrieved successfully.
|
||||||
|
// schema:
|
||||||
|
// type: array
|
||||||
|
// items:
|
||||||
|
// "$ref": "#/definitions/LessonContent"
|
||||||
|
// '500':
|
||||||
|
// description: Failed to retrieve lesson contents.
|
||||||
|
|
||||||
|
var params structs.LessonIdParam
|
||||||
|
|
||||||
|
if err := c.ParamsParser(¶ms); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lessonContents []structs.GetLessonContentsResponse
|
||||||
|
|
||||||
|
database.DB.Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId).Find(&lessonContents)
|
||||||
|
|
||||||
|
// sort contents by position
|
||||||
|
|
||||||
|
sort.SliceStable(lessonContents, func(i, j int) bool {
|
||||||
|
return lessonContents[i].Position < lessonContents[j].Position
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.JSON(lessonContents)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLessonSettings(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation GET /v1/lessons/{lessonId}/settings lessons GetLessonSettings
|
||||||
|
// ---
|
||||||
|
// summary: Get lesson settings.
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: lessonId
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Lesson settings retrieved successfully.
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/LessonSettings"
|
||||||
|
// '500':
|
||||||
|
// description: Failed to retrieve lesson settings.
|
||||||
|
|
||||||
|
var params structs.LessonIdParam
|
||||||
|
|
||||||
|
if err := c.ParamsParser(¶ms); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lessonSettings structs.LessonSettings
|
||||||
|
|
||||||
|
database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).First(&lessonSettings)
|
||||||
|
|
||||||
|
return c.JSON(lessonSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateLessonPreviewTitle(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation PATCH /v1/lessons/{lessonId}/preview/title lessons UpdateLessonPreviewTitle
|
||||||
|
// ---
|
||||||
|
// summary: Update lesson preview title.
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: lessonId
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/UpdateLessonPreviewRequest"
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Lesson preview updated successfully.
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/UpdateLessonPreviewResponse"
|
||||||
|
// '500':
|
||||||
|
// description: Failed to update lesson preview.
|
||||||
|
|
||||||
|
var params structs.LessonIdParam
|
||||||
|
|
||||||
|
if err := c.ParamsParser(¶ms); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body structs.UpdateLessonPreviewTitleRequest
|
||||||
|
|
||||||
|
if err := c.BodyParser(&body); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).Update("title", body.Title)
|
||||||
|
|
||||||
|
return c.JSON(
|
||||||
|
fiber.Map{
|
||||||
|
"message": "Lesson preview updated successfully",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateLessonPreviewThumbnail(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation POST /v1/lessons/{lessonId}/preview/thumbnail lessons UpdateLessonPreviewThumbnail
|
||||||
|
// ---
|
||||||
|
// summary: Update lesson preview thumbnail.
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: lessonId
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/UpdateLessonPreviewThumbnailRequest"
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Lesson preview updated successfully.
|
||||||
|
// '500':
|
||||||
|
// description: Failed to update lesson preview.
|
||||||
|
|
||||||
|
var params structs.LessonIdParam
|
||||||
|
|
||||||
|
if err := c.ParamsParser(¶ms); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileHeader, err := c.FormFile("file")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileHeader.Size > utils.MaxImageSize {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.IsFileTypeAllowed(fileHeader.Header.Get("Content-Type"), utils.AcceptedImageFileTypes) {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get current thumbnail
|
||||||
|
|
||||||
|
lesson := structs.Lesson{}
|
||||||
|
|
||||||
|
database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).First(&lesson)
|
||||||
|
|
||||||
|
// delete current thumbnail
|
||||||
|
|
||||||
|
if lesson.ThumbnailUrl != "" {
|
||||||
|
utils.DeleteFile(lesson.ThumbnailUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := uuid.New().String() + "." + strings.Split(fileHeader.Header["Content-Type"][0], "/")[1]
|
||||||
|
|
||||||
|
databasePath, publicPath := utils.GetFullImagePath(c.Locals("organizationId").(string), params.LessonId)
|
||||||
|
|
||||||
|
utils.CreateFolderStructureIfNotExists(publicPath)
|
||||||
|
|
||||||
|
database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).Update("thumbnail_url", databasePath+fileName)
|
||||||
|
|
||||||
|
return c.SaveFile(fileHeader, publicPath+fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateLessonState(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation PATCH /v1/lessons/{lessonId}/state lessons UpdateLessonState
|
||||||
|
// ---
|
||||||
|
// summary: Update lesson state.
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: lessonId
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/UpdateLessonStateRequest"
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Lesson state updated successfully.
|
||||||
|
// '500':
|
||||||
|
// description: Failed to update lesson state.
|
||||||
|
|
||||||
|
var params structs.LessonIdParam
|
||||||
|
|
||||||
|
if err := c.ParamsParser(¶ms); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body structs.UpdateLessonStateRequest
|
||||||
|
|
||||||
|
if err := c.BodyParser(&body); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).Update("state", body.State)
|
||||||
|
|
||||||
|
return c.JSON(
|
||||||
|
fiber.Map{
|
||||||
|
"message": "Lesson state updated successfully",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddLessonContent(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation POST /v1/lessons/{lessonId}/contents lessons AddLessonContent
|
||||||
|
// ---
|
||||||
|
// summary: Add lesson content.
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: lessonId
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/AddLessonContentRequest"
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Lesson content added successfully.
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/AddLessonContentResponse"
|
||||||
|
// '500':
|
||||||
|
// description: Failed to add lesson content.
|
||||||
|
|
||||||
|
var params structs.LessonIdParam
|
||||||
|
|
||||||
|
if err := c.ParamsParser(¶ms); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body structs.AddLessonContentRequest
|
||||||
|
|
||||||
|
if err := c.BodyParser(&body); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get last position
|
||||||
|
|
||||||
|
var lastContent structs.LessonContent
|
||||||
|
|
||||||
|
database.DB.Select("position").Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId).Order("position desc").First(&lastContent)
|
||||||
|
|
||||||
|
// create new content
|
||||||
|
|
||||||
|
content := structs.LessonContent{
|
||||||
|
Id: uuid.New().String(),
|
||||||
|
LessonId: params.LessonId,
|
||||||
|
Page: 1,
|
||||||
|
Position: lastContent.Position + 1,
|
||||||
|
Type: body.Type,
|
||||||
|
Data: body.Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
database.DB.Create(&content)
|
||||||
|
|
||||||
|
return c.JSON(
|
||||||
|
structs.AddLessonContentResponse{
|
||||||
|
Id: content.Id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadLessonContentFile(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation POST /v1/lessons/{lessonId}/contents/{contentId}/file/{type} lessons UploadLessonContentFile
|
||||||
|
// ---
|
||||||
|
// summary: Upload lesson content file.
|
||||||
|
// consumes:
|
||||||
|
// - multipart/form-data
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: lessonId
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// - name: contentId
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// - name: file
|
||||||
|
// in: formData
|
||||||
|
// required: true
|
||||||
|
// type: file
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Lesson content file uploaded successfully.
|
||||||
|
// '500':
|
||||||
|
// description: Failed to upload lesson content file.
|
||||||
|
|
||||||
|
var params structs.UploadLessonContentFileParam
|
||||||
|
|
||||||
|
if err := c.ParamsParser(¶ms); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileHeader, err := c.FormFile("file")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Type == "image" {
|
||||||
|
if fileHeader.Size > utils.MaxImageSize {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.IsFileTypeAllowed(fileHeader.Header.Get("Content-Type"), utils.AcceptedImageFileTypes) {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
} else if params.Type == "video" {
|
||||||
|
if fileHeader.Size > utils.MaxVideoSize {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.IsFileTypeAllowed(fileHeader.Header.Get("Content-Type"), utils.AcceptedVideoFileTypes) {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get current file
|
||||||
|
|
||||||
|
content := structs.LessonContent{}
|
||||||
|
|
||||||
|
database.DB.Select("type", "data").Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).First(&content)
|
||||||
|
|
||||||
|
// delete current image
|
||||||
|
|
||||||
|
if content.Type == utils.LessonContentTypeImage && content.Data != "" {
|
||||||
|
utils.DeleteFile(content.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := uuid.New().String() + "." + strings.Split(fileHeader.Header["Content-Type"][0], "/")[1]
|
||||||
|
|
||||||
|
databasePath, publicPath := utils.GetFullImagePath(c.Locals("organizationId").(string), params.LessonId)
|
||||||
|
|
||||||
|
utils.CreateFolderStructureIfNotExists(publicPath)
|
||||||
|
|
||||||
|
database.DB.Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).Update("data", databasePath+fileName)
|
||||||
|
|
||||||
|
if err := c.SaveFile(fileHeader, publicPath+fileName); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
|
"error": "Failed to save file",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"Data": databasePath + fileName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateLessonContent(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation PATCH /v1/lessons/{lessonId}/contents/{contentId} lessons UpdateLessonContent
|
||||||
|
// ---
|
||||||
|
// summary: Update lesson content.
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: lessonId
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// - name: contentId
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/UpdateLessonContentRequest"
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Lesson content updated successfully.
|
||||||
|
// '500':
|
||||||
|
// description: Failed to update lesson content.
|
||||||
|
|
||||||
|
var params structs.LessonIdAndContentIdParam
|
||||||
|
|
||||||
|
if err := c.ParamsParser(¶ms); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body structs.UpdateLessonContentRequest
|
||||||
|
|
||||||
|
if err := c.BodyParser(&body); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
database.DB.Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).Update("data", body.Data)
|
||||||
|
|
||||||
|
return c.JSON(
|
||||||
|
fiber.Map{
|
||||||
|
"message": "Lesson content updated successfully",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateLessonContentPosition(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation PATCH /v1/lessons/{lessonId}/contents/{contentId}/position lessons UpdateLessonContentPosition
|
||||||
|
// ---
|
||||||
|
// summary: Update lesson content position.
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: lessonId
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// - name: contentId
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/UpdateLessonContentPositionRequest"
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Lesson content position updated successfully.
|
||||||
|
// '500':
|
||||||
|
// description: Failed to update lesson content position.
|
||||||
|
|
||||||
|
var params structs.LessonIdAndContentIdParam
|
||||||
|
|
||||||
|
if err := c.ParamsParser(¶ms); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body structs.UpdateLessonContentPositionRequest
|
||||||
|
|
||||||
|
if err := c.BodyParser(&body); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update position
|
||||||
|
|
||||||
|
newPosition := body.Position
|
||||||
|
|
||||||
|
// Begin a transaction to ensure consistency
|
||||||
|
tx := database.DB.Begin()
|
||||||
|
if tx.Error != nil {
|
||||||
|
return c.SendStatus(fiber.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the current position of the content being moved
|
||||||
|
var currentContent structs.LessonContent
|
||||||
|
if err := tx.Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).First(¤tContent).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return c.SendStatus(fiber.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldPosition := currentContent.Position
|
||||||
|
|
||||||
|
if oldPosition == newPosition {
|
||||||
|
// No need to update if the position hasn't changed
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"message": "Lesson content position updated successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldPosition < newPosition {
|
||||||
|
// Items between oldPosition and newPosition need to be shifted down
|
||||||
|
if err := tx.Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId).
|
||||||
|
Where("position > ? AND position <= ?", oldPosition, newPosition).
|
||||||
|
Update("position", gorm.Expr("position - 1")).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return c.SendStatus(fiber.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Items between newPosition and oldPosition need to be shifted up
|
||||||
|
if err := tx.Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId).
|
||||||
|
Where("position >= ? AND position < ?", newPosition, oldPosition).
|
||||||
|
Update("position", gorm.Expr("position + 1")).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return c.SendStatus(fiber.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the position of the moved content
|
||||||
|
if err := tx.Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).Update("position", newPosition).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return c.SendStatus(fiber.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(
|
||||||
|
fiber.Map{
|
||||||
|
"message": "Lesson content position updated successfully",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteLessonContent(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation DELETE /v1/lessons/{lessonId}/contents/{contentId} lessons DeleteLessonContent
|
||||||
|
// ---
|
||||||
|
// summary: Delete lesson content.
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: lessonId
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// - name: contentId
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Lesson content deleted successfully.
|
||||||
|
// '500':
|
||||||
|
// description: Failed to delete lesson content.
|
||||||
|
|
||||||
|
var params structs.LessonIdAndContentIdParam
|
||||||
|
|
||||||
|
if err := c.ParamsParser(¶ms); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin a transaction to ensure consistency
|
||||||
|
tx := database.DB.Begin()
|
||||||
|
if tx.Error != nil {
|
||||||
|
return c.SendStatus(fiber.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the current position of the content being deleted
|
||||||
|
var content structs.LessonContent
|
||||||
|
if err := tx.Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).First(&content).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return c.SendStatus(fiber.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the content
|
||||||
|
if err := tx.Delete(&content).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return c.SendStatus(fiber.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift down the positions of all contents with a higher position
|
||||||
|
if err := tx.Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId).
|
||||||
|
Where("position > ?", content.Position).
|
||||||
|
Update("position", gorm.Expr("position - 1")).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return c.SendStatus(fiber.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(
|
||||||
|
fiber.Map{
|
||||||
|
"message": "Lesson content deleted successfully",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
package organization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.ex.umbach.dev/Alex/roese-utils/rsutils"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"lms.de/backend/modules/database"
|
||||||
|
"lms.de/backend/modules/structs"
|
||||||
|
"lms.de/backend/modules/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateOrganization(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation POST /organization organization createOrganization
|
||||||
|
// ---
|
||||||
|
// summary: Create organization
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/CreateOrganizationRequest"
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Organization created successfully
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/CreateOrganizationResponse"
|
||||||
|
// '400':
|
||||||
|
// description: Invalid request body
|
||||||
|
// '500':
|
||||||
|
// description: Failed to create organization
|
||||||
|
|
||||||
|
var body structs.CreateOrganizationRequest
|
||||||
|
|
||||||
|
if err := rsutils.BodyParserHelper(c, &body); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedPassword, err := base64.StdEncoding.DecodeString(body.Password)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msg("Failed to decode base64 password, err: " + err.Error())
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if passwordValid := utils.IsPasswordLengthValid(string(decodedPassword)); !passwordValid {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword(decodedPassword, bcrypt.DefaultCost)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msg("Failed to hash password, err: " + err.Error())
|
||||||
|
return c.SendStatus(fiber.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
organizationId := uuid.New().String()
|
||||||
|
userId := uuid.New().String()
|
||||||
|
subdomain := utils.GenerateSubdomain()
|
||||||
|
|
||||||
|
database.DB.Create(&structs.Organization{
|
||||||
|
Id: organizationId,
|
||||||
|
Subdomain: subdomain,
|
||||||
|
OwnerUserId: userId,
|
||||||
|
CompanyName: "Mustermann GmbH",
|
||||||
|
})
|
||||||
|
|
||||||
|
database.DB.Create(&structs.User{
|
||||||
|
Id: userId,
|
||||||
|
OrganizationId: organizationId,
|
||||||
|
Active: true,
|
||||||
|
FirstName: "Max",
|
||||||
|
LastName: "Mustermann",
|
||||||
|
Email: body.Email,
|
||||||
|
Password: string(hashedPassword),
|
||||||
|
})
|
||||||
|
|
||||||
|
session, err := rsutils.GenerateSession()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
database.DB.Create(&structs.UserSession{
|
||||||
|
Id: session,
|
||||||
|
OrganizationId: organizationId,
|
||||||
|
Session: uuid.New().String(),
|
||||||
|
UserId: userId,
|
||||||
|
UserAgent: string(c.Context().UserAgent()),
|
||||||
|
ExpiresAt: utils.GetSessionExpiresAtTime(),
|
||||||
|
LastUsedAt: time.Now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.JSON(structs.CreateOrganizationResponse{
|
||||||
|
OrganizationSubdomain: subdomain,
|
||||||
|
Session: session,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package organization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"lms.de/backend/modules/database"
|
||||||
|
"lms.de/backend/modules/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetOrganizationSettings(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation GET /organization/settings organization getOrganizationSettings
|
||||||
|
// ---
|
||||||
|
// summary: Get settings
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Settings fetched successfully
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/GetOrganizationSettingsResponse"
|
||||||
|
// '400':
|
||||||
|
// description: Invalid request body
|
||||||
|
// '500':
|
||||||
|
// description: Failed to fetch settings
|
||||||
|
|
||||||
|
var organizationSettings structs.GetOrganizationSettingsResponse
|
||||||
|
|
||||||
|
database.DB.Model(&structs.Organization{}).Select("subdomain", "company_name", "primary_color", "logo_url", "banner_url").First(&organizationSettings)
|
||||||
|
|
||||||
|
return c.JSON(organizationSettings)
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package organization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"lms.de/backend/modules/database"
|
||||||
|
"lms.de/backend/modules/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetTeamMembers(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation GET /organization/team/members organization getTeamMembers
|
||||||
|
// ---
|
||||||
|
// summary: Get team members
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Team members fetched successfully
|
||||||
|
// schema:
|
||||||
|
// type: array
|
||||||
|
// items:
|
||||||
|
// "$ref": "#/definitions/TeamMember"
|
||||||
|
// '400':
|
||||||
|
// description: Invalid request body
|
||||||
|
// '500':
|
||||||
|
// description: Failed to fetch team members
|
||||||
|
|
||||||
|
var users []structs.TeamMember
|
||||||
|
|
||||||
|
database.DB.Model(&structs.User{}).Where("organization_id = ?", c.Locals("organizationId")).Find(&users)
|
||||||
|
|
||||||
|
return c.JSON(users)
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"git.ex.umbach.dev/Alex/roese-utils/rslogger"
|
||||||
|
"git.ex.umbach.dev/Alex/roese-utils/rsutils"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"lms.de/backend/modules/database"
|
||||||
|
"lms.de/backend/modules/logger"
|
||||||
|
"lms.de/backend/modules/structs"
|
||||||
|
"lms.de/backend/modules/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UserLogin(c *fiber.Ctx) error {
|
||||||
|
// swagger:operation POST /user/auth/login user userLogin
|
||||||
|
// ---
|
||||||
|
// summary: Login user
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/UserLoginRequest"
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: User logged in successfully
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/UserLoginResponse"
|
||||||
|
// '400':
|
||||||
|
// description: Invalid request body
|
||||||
|
// '401':
|
||||||
|
// description: Incorrect password or user deactivated
|
||||||
|
// '500':
|
||||||
|
// description: Failed to login user
|
||||||
|
|
||||||
|
var body structs.UserLoginRequest
|
||||||
|
|
||||||
|
if err := rsutils.BodyParserHelper(c, &body); err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedPassword, err := base64.StdEncoding.DecodeString(body.Password)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msg("Failed to decode base64 password, err: " + err.Error())
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if passwordValid := utils.IsPasswordLengthValid(string(decodedPassword)); !passwordValid {
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
var user structs.User
|
||||||
|
|
||||||
|
organizationId := c.Locals("organizationId").(string)
|
||||||
|
|
||||||
|
database.DB.Select("id", "active", "password").First(&user, "email = ? AND organization_id = ?", body.Email, organizationId)
|
||||||
|
|
||||||
|
if user.Id == "" {
|
||||||
|
log.Error().Msg("User not found")
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), decodedPassword); err != nil {
|
||||||
|
log.Error().Msg("Incorrect password")
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.Active {
|
||||||
|
return c.SendStatus(fiber.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := rsutils.GenerateSession()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return c.SendStatus(fiber.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
database.DB.Create(&structs.UserSession{
|
||||||
|
Id: uuid.New().String(),
|
||||||
|
OrganizationId: organizationId,
|
||||||
|
Session: session,
|
||||||
|
UserId: user.Id,
|
||||||
|
UserAgent: string(c.Context().UserAgent()),
|
||||||
|
ExpiresAt: utils.GetSessionExpiresAtTime()})
|
||||||
|
|
||||||
|
logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has logged in", user.Id)
|
||||||
|
|
||||||
|
return c.JSON(structs.UserLoginResponse{Session: session})
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"lms.de/backend/modules/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetUser(c *fiber.Ctx) error {
|
||||||
|
|
||||||
|
return c.JSON(structs.GetUserResponse{
|
||||||
|
AvatarUrl: "",
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"lms.de/backend/modules/config"
|
||||||
|
"lms.de/backend/modules/database"
|
||||||
|
"lms.de/backend/modules/structs"
|
||||||
|
"lms.de/backend/modules/utils"
|
||||||
|
"lms.de/backend/routers/router/api/v1/lessons"
|
||||||
|
"lms.de/backend/routers/router/api/v1/organization"
|
||||||
|
"lms.de/backend/routers/router/api/v1/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupRoutes(app *fiber.App) {
|
||||||
|
v1 := app.Group("/v1")
|
||||||
|
|
||||||
|
o := v1.Group("/organization")
|
||||||
|
o.Post("/", organization.CreateOrganization)
|
||||||
|
o.Get("/team/members", handleOrganizationSubdomain, requestAccessValidation, organization.GetTeamMembers)
|
||||||
|
o.Get("/settings", handleOrganizationSubdomain, requestAccessValidation, organization.GetOrganizationSettings)
|
||||||
|
|
||||||
|
u := v1.Group("/user")
|
||||||
|
u.Get("/", handleOrganizationSubdomain, requestAccessValidation, user.GetUser)
|
||||||
|
u.Post("/auth/login", handleOrganizationSubdomain, user.UserLogin)
|
||||||
|
|
||||||
|
l := v1.Group("/lessons")
|
||||||
|
l.Get("/", handleOrganizationSubdomain, requestAccessValidation, lessons.GetLessons)
|
||||||
|
l.Post("/", handleOrganizationSubdomain, requestAccessValidation, lessons.CreateLesson)
|
||||||
|
l.Get("/:lessonId/contents", handleOrganizationSubdomain, requestAccessValidation, lessons.GetLessonContents)
|
||||||
|
l.Get("/:lessonId/settings", handleOrganizationSubdomain, requestAccessValidation, lessons.GetLessonSettings)
|
||||||
|
l.Patch("/:lessonId/preview/title", handleOrganizationSubdomain, requestAccessValidation, lessons.UpdateLessonPreviewTitle)
|
||||||
|
l.Post("/:lessonId/preview/thumbnail", handleOrganizationSubdomain, requestAccessValidation, lessons.UpdateLessonPreviewThumbnail)
|
||||||
|
l.Patch("/:lessonId/state", handleOrganizationSubdomain, requestAccessValidation, lessons.UpdateLessonState)
|
||||||
|
l.Post("/:lessonId/contents", handleOrganizationSubdomain, requestAccessValidation, lessons.AddLessonContent)
|
||||||
|
l.Patch("/:lessonId/contents/:contentId", handleOrganizationSubdomain, requestAccessValidation, lessons.UpdateLessonContent)
|
||||||
|
l.Patch("/:lessonId/contents/:contentId/position", handleOrganizationSubdomain, requestAccessValidation, lessons.UpdateLessonContentPosition)
|
||||||
|
l.Delete("/:lessonId/contents/:contentId", handleOrganizationSubdomain, requestAccessValidation, lessons.DeleteLessonContent)
|
||||||
|
l.Post("/:lessonId/contents/:contentId/file/:type", handleOrganizationSubdomain, requestAccessValidation, lessons.UploadLessonContentFile)
|
||||||
|
|
||||||
|
app.Static("/static", config.Cfg.FolderPaths.PublicStatic)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userSessionValidation(c *fiber.Ctx) error {
|
||||||
|
xAuthorization := utils.GetXAuhorizationHeader(c)
|
||||||
|
|
||||||
|
if len(xAuthorization) != utils.LenHeaderXAuthorization {
|
||||||
|
return fiber.ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
var userSession structs.UserSession
|
||||||
|
|
||||||
|
database.DB.Select("session", "user_id").First(&userSession, "session = ? AND organization_id = ?", xAuthorization, c.Locals("organizationId"))
|
||||||
|
|
||||||
|
if userSession.Session != xAuthorization {
|
||||||
|
return fiber.ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Locals("userId", userSession.UserId)
|
||||||
|
c.Locals("organizationId", c.Locals("organizationId"))
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestAccessValidation(c *fiber.Ctx) error {
|
||||||
|
// user session
|
||||||
|
xAuthorization := utils.GetXAuhorizationHeader(c)
|
||||||
|
|
||||||
|
if len(xAuthorization) == utils.LenHeaderXAuthorization {
|
||||||
|
return userSessionValidation(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// api key
|
||||||
|
/*xApiKey := utils.GetXApiKeyHeader(c)
|
||||||
|
|
||||||
|
if len(xApiKey) == utils.LenHeaderXApiKey {
|
||||||
|
return userApikeyTokenValidation(c)
|
||||||
|
} */
|
||||||
|
|
||||||
|
return c.SendStatus(fiber.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
// gets the organization id by subdomain and sets it in the locals
|
||||||
|
func handleOrganizationSubdomain(c *fiber.Ctx) error {
|
||||||
|
host := c.Hostname()
|
||||||
|
|
||||||
|
// split the hostname into parts
|
||||||
|
parts := strings.Split(host, ".")
|
||||||
|
|
||||||
|
// check if we have at least three parts (subdomain, domain, tld)
|
||||||
|
if len(parts) >= 3 {
|
||||||
|
// the first part is the subdomain
|
||||||
|
subdomain := parts[0]
|
||||||
|
|
||||||
|
// get organization id by subdomain from database
|
||||||
|
organization := structs.Organization{}
|
||||||
|
|
||||||
|
database.DB.Select("id").First(&organization, "subdomain = ?", subdomain)
|
||||||
|
|
||||||
|
// if organization not found
|
||||||
|
if organization.Id == "" {
|
||||||
|
return c.SendStatus(fiber.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Locals("organizationId", organization.Id)
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SendStatus(fiber.StatusBadRequest)
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
package socketserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.ex.umbach.dev/Alex/roese-utils/rslogger"
|
||||||
|
"github.com/gofiber/websocket/v2"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"lms.de/backend/modules/cache"
|
||||||
|
"lms.de/backend/modules/database"
|
||||||
|
"lms.de/backend/modules/logger"
|
||||||
|
"lms.de/backend/modules/structs"
|
||||||
|
"lms.de/backend/modules/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var register = make(chan *structs.SocketClient)
|
||||||
|
var broadcast = make(chan structs.SocketMessage)
|
||||||
|
var unregister = make(chan *websocket.Conn)
|
||||||
|
|
||||||
|
func RunHub() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case newSocketClient := <-register:
|
||||||
|
userId := fmt.Sprintf("%v", newSocketClient.Conn.Locals("userId"))
|
||||||
|
browserTabSession := fmt.Sprintf("%v", newSocketClient.Conn.Locals("browserTabSession"))
|
||||||
|
sessionId := fmt.Sprintf("%v", newSocketClient.Conn.Locals("sessionId"))
|
||||||
|
|
||||||
|
// close connection instantly if sessionId is empty
|
||||||
|
if sessionId == "<nil>" {
|
||||||
|
newSocketClient.SendUnauthorizedCloseMessage()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newSocketClient.SessionId = sessionId
|
||||||
|
newSocketClient.BrowserTabSession = browserTabSession
|
||||||
|
newSocketClient.UserId = userId
|
||||||
|
|
||||||
|
cache.AddSocketClient(newSocketClient)
|
||||||
|
|
||||||
|
// check that user session is not expired
|
||||||
|
var userSession structs.UserSession
|
||||||
|
|
||||||
|
database.DB.Select("expires_at").First(&userSession, "session = ?", sessionId)
|
||||||
|
|
||||||
|
if !userSession.ExpiresAt.IsZero() && time.Now().After(userSession.ExpiresAt) {
|
||||||
|
newSocketClient.SendUnauthorizedCloseMessage()
|
||||||
|
database.DB.Delete(&structs.UserSession{}, "session = ?", sessionId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// update session last used time
|
||||||
|
database.DB.Model(&structs.UserSession{}).Where("session = ?", sessionId).Updates(structs.UserSession{
|
||||||
|
LastUsedAt: time.Now(),
|
||||||
|
ExpiresAt: utils.GetSessionExpiresAtTime(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// socketclients.UpdateUserSessionsForUser(userId, sessionId)
|
||||||
|
|
||||||
|
logger.AddSystemLog(rslogger.LogTypeInfo, "User %v has come online", userId)
|
||||||
|
|
||||||
|
case data := <-broadcast:
|
||||||
|
var receivedMessage structs.ReceivedMessage
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data.Msg, &receivedMessage); err != nil {
|
||||||
|
log.Error().Msgf("Failed to unmarshal received msg, err: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("Received message: %v %v", receivedMessage, receivedMessage.Cmd)
|
||||||
|
|
||||||
|
switch receivedMessage.Cmd {
|
||||||
|
case 1:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
log.Error().Msgf("Received unknown message: %v", receivedMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
case connection := <-unregister:
|
||||||
|
cache.DeleteClientByConn(connection)
|
||||||
|
|
||||||
|
if connection.Locals("userId") != nil && connection.Locals("sessionId") != nil {
|
||||||
|
userId := connection.Locals("userId").(string)
|
||||||
|
// sessionId := connection.Locals("sessionId").(string)
|
||||||
|
|
||||||
|
database.DB.Model(&structs.User{}).Where("id = ?", userId).Updates(structs.User{
|
||||||
|
LastOnlineAt: time.Now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// socketclients.UpdateUserSessionsForUser(userId, sessionId)
|
||||||
|
|
||||||
|
logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has gone offline", userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package socketserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/websocket/v2"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"lms.de/backend/modules/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WebSocketServer(app *fiber.App) {
|
||||||
|
app.Get("/ws", websocket.New(func(c *websocket.Conn) {
|
||||||
|
defer func() {
|
||||||
|
unregister <- c
|
||||||
|
c.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
register <- &structs.SocketClient{Conn: c}
|
||||||
|
|
||||||
|
for {
|
||||||
|
messageType, msg, err := c.ReadMessage()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||||
|
log.Error().Msgf("Read err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if messageType == websocket.TextMessage {
|
||||||
|
broadcast <- structs.SocketMessage{Conn: c, Msg: msg}
|
||||||
|
} else {
|
||||||
|
log.Error().Msgf("websocket message received of type %v", messageType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|