init project

main
alex 2024-09-03 23:48:12 +02:00
commit d35d43a494
86 changed files with 2164 additions and 0 deletions

7
commit_and_push.sh Executable file
View File

@ -0,0 +1,7 @@
git add *
read -p "Commit message: " commit_message
git commit -m "$commit_message"
git push -u origin main

40
go.mod Normal file
View File

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

90
go.sum Normal file
View File

@ -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=

111
main.go Normal file
View File

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

53
modules/cache/socketclient.go vendored Normal file
View File

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

69
modules/config/config.go Normal file
View File

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

View File

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

14
modules/logger/logger.go Normal file
View File

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

108
modules/structs/lessons.go Normal file
View File

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

View File

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

View File

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

13
modules/structs/roles.go Normal file
View File

@ -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)"`
}

104
modules/structs/socket.go Normal file
View File

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

56
modules/structs/users.go Normal file
View File

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

40
modules/utils/globals.go Normal file
View File

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

130
modules/utils/utils.go Normal file
View File

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

View File

@ -0,0 +1,5 @@
package utils
func ValidatorInit() {
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -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(&params); 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(&params); 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(&params); 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(&params); 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(&params); 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(&params); 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(&params); 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(&params); 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(&params); 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(&currentContent).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(&params); 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",
},
)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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: "",
})
}

112
routers/router/router.go Normal file
View File

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

98
socketserver/hub.go Normal file
View File

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

37
socketserver/server.go Normal file
View File

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