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)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|