diff --git a/go.mod b/go.mod index 9af116a..3fc023e 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( ) require ( + git.ex.umbach.dev/LMS/libcore v1.0.6 // indirect 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 diff --git a/go.sum b/go.sum index 32b5e91..257c870 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,13 @@ 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= +git.ex.umbach.dev/LMS/libcore v1.0.0 h1:0vhIxeFNHdo4ftVHEGxZ35Mf0jkRuSs9QkfelWFDySc= +git.ex.umbach.dev/LMS/libcore v1.0.0/go.mod h1:BvbMJWYQ83dblDAB4ooLfwuorjdy6G3txdOrjRfIKPA= +git.ex.umbach.dev/LMS/libcore v1.0.1 h1:J9sRarL6OJ4JOaE8UH1yykOYdjq5H6TehMDq86269iA= +git.ex.umbach.dev/LMS/libcore v1.0.1/go.mod h1:BvbMJWYQ83dblDAB4ooLfwuorjdy6G3txdOrjRfIKPA= +git.ex.umbach.dev/LMS/libcore v1.0.2 h1:RvE0e+Eja/9HOU5reEG2UU18mgDgKYD8gXJOrOntRmc= +git.ex.umbach.dev/LMS/libcore v1.0.2/go.mod h1:BvbMJWYQ83dblDAB4ooLfwuorjdy6G3txdOrjRfIKPA= +git.ex.umbach.dev/LMS/libcore v1.0.6 h1:Af+2jD4aC3+4qMgSmTn4nvRosAS5ETTX9JR3gznlGPI= +git.ex.umbach.dev/LMS/libcore v1.0.6/go.mod h1:BvbMJWYQ83dblDAB4ooLfwuorjdy6G3txdOrjRfIKPA= 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= diff --git a/main.go b/main.go index 96a653d..8cb6175 100644 --- a/main.go +++ b/main.go @@ -7,13 +7,14 @@ import ( "git.ex.umbach.dev/Alex/roese-utils/rsconfig" "git.ex.umbach.dev/Alex/roese-utils/rslogger" + "git.ex.umbach.dev/LMS/libcore/models" "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/permissions" "lms.de/backend/modules/utils" "lms.de/backend/routers/router" "lms.de/backend/socketserver" @@ -51,6 +52,7 @@ MARIADB_DATABASE_NAME=db_database_name`) utils.ValidatorInit() database.InitDatabase() + permissions.InitPermissions() } func main() { @@ -79,12 +81,12 @@ func main() { } // validate ws session - var userSession structs.UserSession + var userSession models.UserSession database.DB.Select("user_id").First(&userSession, "session = ?", sessionId) if userSession.UserId != "" { - var user structs.User + var user models.User database.DB.First(&user, "id = ?", userSession.UserId) diff --git a/modules/cache/permissions.go b/modules/cache/permissions.go new file mode 100644 index 0000000..25aecef --- /dev/null +++ b/modules/cache/permissions.go @@ -0,0 +1,19 @@ +package cache + +import "sync" + +var masterRolePermissions []uint16 +var muMRP sync.RWMutex + +func SetMasterRolePermissions(permissions []uint16) { + muMRP.Lock() + masterRolePermissions = permissions + muMRP.Unlock() +} + +func GetMasterRolePermissions() []uint16 { + muMRP.RLock() + defer muMRP.RUnlock() + + return masterRolePermissions +} diff --git a/modules/database/database.go b/modules/database/database.go index 698f386..2731365 100644 --- a/modules/database/database.go +++ b/modules/database/database.go @@ -3,11 +3,11 @@ package database import ( "fmt" + "git.ex.umbach.dev/LMS/libcore/models" "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 @@ -35,15 +35,15 @@ func InitDatabase() { 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{}) + db.AutoMigrate(&models.Organization{}) + db.AutoMigrate(&models.Role{}) + db.AutoMigrate(&models.RolePermission{}) + db.AutoMigrate(&models.User{}) + db.AutoMigrate(&models.UserSession{}) + db.AutoMigrate(&models.Lesson{}) + db.AutoMigrate(&models.LessonContent{}) + db.AutoMigrate(&models.Question{}) + db.AutoMigrate(&models.QuestionLike{}) + db.AutoMigrate(&models.QuestionReply{}) + db.AutoMigrate(&models.QuestionReplyLike{}) } diff --git a/modules/permissions/permissions.go b/modules/permissions/permissions.go new file mode 100644 index 0000000..5d142f9 --- /dev/null +++ b/modules/permissions/permissions.go @@ -0,0 +1,15 @@ +package permissions + +import ( + "lms.de/backend/modules/cache" + "lms.de/backend/modules/database" + "lms.de/backend/modules/utils" +) + +func InitPermissions() { + cache.SetMasterRolePermissions(utils.Permissions) + + // delete permission from database if not longer in permissions + + database.DB.Exec("DELETE FROM role_permissions WHERE permission NOT IN (?)", utils.Permissions) +} diff --git a/modules/structs/lessons.go b/modules/structs/lessons.go index 426b349..f24183c 100644 --- a/modules/structs/lessons.go +++ b/modules/structs/lessons.go @@ -7,28 +7,6 @@ const ( 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 diff --git a/modules/structs/organizations.go b/modules/structs/organizations.go index 6e13523..0181cd8 100644 --- a/modules/structs/organizations.go +++ b/modules/structs/organizations.go @@ -1,20 +1,5 @@ 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 diff --git a/modules/structs/questions.go b/modules/structs/questions.go index 768b712..c025054 100644 --- a/modules/structs/questions.go +++ b/modules/structs/questions.go @@ -1,38 +1 @@ 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 -} diff --git a/modules/structs/roles.go b/modules/structs/roles.go index 4c27183..8a1b15e 100644 --- a/modules/structs/roles.go +++ b/modules/structs/roles.go @@ -1,13 +1,38 @@ 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 RolesResponse struct { + Roles []HelperRole } -type RolePermission struct { - Id string `gorm:"primaryKey;type:varchar(36)"` - RoleId string `gorm:"type:varchar(36)"` - Permission string `gorm:"type:varchar(255)"` +type HelperRole struct { + Id string + Permissions []uint16 + Users []HelperRoleUser } + +type HelperRoleUser struct { + FirstName string + LastName string + ProfilePictureUrl string +} + +/* +type HelperRole struct { + Id string + Name string + Master bool + Permissions []uint16 + Users []HelperRoleUser +} + +type HelperRoleUser struct { + FirstName string + LastName string + ProfilePictureUrl string +} + +// swagger:model CreateRoleRequest +type CreateRoleRequest struct { + Name string +} +*/ diff --git a/modules/structs/socket.go b/modules/structs/socket.go index 5505c42..bcef330 100644 --- a/modules/structs/socket.go +++ b/modules/structs/socket.go @@ -19,6 +19,7 @@ const ( type SocketClient struct { SessionId string BrowserTabSession string + OrganizationId string UserId string Conn *websocket.Conn connMu sync.Mutex diff --git a/modules/structs/users.go b/modules/structs/users.go index ef81d8c..edc839c 100644 --- a/modules/structs/users.go +++ b/modules/structs/users.go @@ -1,35 +1,5 @@ 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 } @@ -54,3 +24,11 @@ type TeamMember struct { RoleId string ProfilePictureUrl string } + +type CreateTeamMemberRequest struct { + FirstName string + LastName string + Email string + RoleId string + Password string +} diff --git a/modules/utils/globals.go b/modules/utils/globals.go index cbd6149..4b9e9d5 100644 --- a/modules/utils/globals.go +++ b/modules/utils/globals.go @@ -6,13 +6,15 @@ const ( maxPassword = "64" MaxPassword = 64 - LenHeaderXAuthorization = 36 - lenHeaderXAuthorization = "36" - LenUserId = 36 - LenHeaderXApiKey = 36 + LenHeaderXAuthorization = 36 + lenHeaderXAuthorization = "36" + LenUserId = 36 + LenHeaderXApiKey = 36 + LenHeaderBrowserTabSession = 36 - HeaderXAuthorization = "X-Authorization" - HeaderXApiKey = "X-Api-Key" + HeaderXAuthorization = "X-Authorization" + HeaderXApiKey = "X-Api-Key" + HeaderBrowserTabSession = "Browser-Tab-Session" SessionExpiresAtTime = 7 * 24 * 60 * 60 // 1 week @@ -38,3 +40,59 @@ const ( LessonContentTypeText = 2 LessonContentTypeImage ) + +const ( + PermissionTeamInviteNewTeamMember = 1 + PermissionTeamRemoveTeamMember = 2 +) + +var Permissions = []uint16{ + PermissionTeamInviteNewTeamMember, + PermissionTeamRemoveTeamMember, +} + +const ( + RoleAdminId = "d0f0fa0d-3f3b-438b-a76f-7febeb8aab57" + RoleModeratorId = "b7359e12-359e-423b-b39c-f0d4069adebc" + RoleUserId = "a1f084ad-d501-4015-b326-4c5c46fd1c5e" +) + +// Permissions for each role + +var AdminPermissions = Permissions + +var ModeratorPermissions = []uint16{ + PermissionTeamInviteNewTeamMember, +} + +var UserPermissions = []uint16{} + +var Roles = map[string][]uint16{ + RoleAdminId: AdminPermissions, + RoleModeratorId: ModeratorPermissions, + RoleUserId: UserPermissions, +} + +// websocket + +// commands sent to websocket clients +const ( + SendCmdSettingsUpdated = 1 + SendCmdSettingsUpdatedLogo = 2 + SendCmdSettingsUpdatedBanner = 3 + SendCmdSettingsUpdatedSubdomain = 4 + SendCmdTeamAddedMember = 5 +) + +// commands received from websocket clients +const ( + ReceivedCmdSubscribeToTopic = 1 +) + +const ( + SubscribedTopicLessons = "/lessons" + SubscribedTopicTeam = "/team" + SubscribedTopicRoles = "/roles" + SubscribedTopicSettings = "/settings" + SubscribedTopicAccount = "/account" +) diff --git a/modules/utils/utils.go b/modules/utils/utils.go index 687d0e3..21843ed 100644 --- a/modules/utils/utils.go +++ b/modules/utils/utils.go @@ -7,11 +7,11 @@ import ( "os" "time" + "git.ex.umbach.dev/LMS/libcore/models" "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 { @@ -32,6 +32,15 @@ func GetXApiKeyHeader(c *fiber.Ctx) string { return c.GetReqHeaders()[HeaderXApiKey][0] } +func GetBrowserTabSessionHeader(c *fiber.Ctx) string { + // check if header is set + if len(c.GetReqHeaders()[HeaderBrowserTabSession]) == 0 { + return "" + } + + return c.GetReqHeaders()[HeaderBrowserTabSession][0] +} + func GetSessionExpiresAtTime() time.Time { return time.Now().Add(time.Second * SessionExpiresAtTime) } @@ -71,7 +80,7 @@ func GenerateSubdomain() string { } func IsSubdomainAlreadyInUse(subdomain string) bool { - var organization structs.Organization + var organization models.Organization if err := database.DB.Where("subdomain = ?", subdomain).First(&organization).Error; err != nil { return false diff --git a/public/demo/logo.png b/public/demo/logo.png new file mode 100644 index 0000000..3a1c4d3 Binary files /dev/null and b/public/demo/logo.png differ diff --git a/routers/router/api/v1/app/app.go b/routers/router/api/v1/app/app.go index b028037..5a1e5ab 100644 --- a/routers/router/api/v1/app/app.go +++ b/routers/router/api/v1/app/app.go @@ -1,6 +1,7 @@ package app import ( + "git.ex.umbach.dev/LMS/libcore/models" "github.com/gofiber/fiber/v2" "lms.de/backend/modules/database" "lms.de/backend/modules/structs" @@ -24,17 +25,15 @@ func GetApp(c *fiber.Ctx) error { // '500': // description: Failed to fetch app - var user structs.User + var user models.User - database.DB.Model(&structs.User{ + database.DB.Model(&models.User{ Id: c.Locals("userId").(string), }).Select("profile_picture_url").First(&user) - var organization structs.Organization + var organization models.Organization - database.DB.Model(&structs.Organization{ - Id: c.Locals("organizationId").(string), - }).Select("company_name", "primary_color", "logo_url", "banner_url").First(&organization) + database.DB.Model(&models.Organization{}).Select("company_name", "primary_color", "logo_url", "banner_url").Where("id = ?", c.Locals("organizationId").(string)).First(&organization) return c.JSON(structs.GetAppResponse{ User: structs.AppUser{ diff --git a/routers/router/api/v1/lessons/lessons.go b/routers/router/api/v1/lessons/lessons.go index bfea37f..bf5a2a1 100644 --- a/routers/router/api/v1/lessons/lessons.go +++ b/routers/router/api/v1/lessons/lessons.go @@ -4,6 +4,7 @@ import ( "sort" "strings" + "git.ex.umbach.dev/LMS/libcore/models" "github.com/gofiber/fiber/v2" "github.com/google/uuid" "gorm.io/gorm" @@ -32,7 +33,9 @@ func GetLessons(c *fiber.Ctx) error { var lessons []structs.LessonResponse - database.DB.Model(&structs.Lesson{}).Where("organization_id = ?", c.Locals("organizationId")).Find(&lessons) + if err := database.DB.Model(&models.Lesson{}).Where("organization_id = ?", c.Locals("organizationId")).Find(&lessons).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } return c.JSON(lessons) } @@ -53,7 +56,7 @@ func CreateLesson(c *fiber.Ctx) error { // '500': // description: Failed to create lesson. - lesson := structs.Lesson{ + lesson := models.Lesson{ Id: uuid.New().String(), OrganizationId: c.Locals("organizationId").(string), State: structs.LessonStateDraft, @@ -61,7 +64,9 @@ func CreateLesson(c *fiber.Ctx) error { CreatorUserId: c.Locals("userId").(string), } - database.DB.Create(&lesson) + if err := database.DB.Create(&lesson).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } return c.JSON( structs.CreateLessonResponse{ @@ -101,7 +106,9 @@ func GetLessonContents(c *fiber.Ctx) error { var lessonContents []structs.GetLessonContentsResponse - database.DB.Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId).Find(&lessonContents) + if err := database.DB.Model(&models.LessonContent{}).Where("lesson_id = ?", params.LessonId).Find(&lessonContents).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } // sort contents by position @@ -141,7 +148,9 @@ func GetLessonSettings(c *fiber.Ctx) error { var lessonSettings structs.LessonSettings - database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).First(&lessonSettings) + if err := database.DB.Model(&models.Lesson{}).Where("id = ?", params.LessonId).First(&lessonSettings).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } return c.JSON(lessonSettings) } @@ -183,7 +192,9 @@ func UpdateLessonPreviewTitle(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusBadRequest) } - database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).Update("title", body.Title) + if err := database.DB.Model(&models.Lesson{}).Where("id = ?", params.LessonId).Update("title", body.Title); err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } return c.JSON( fiber.Map{ @@ -237,9 +248,11 @@ func UpdateLessonPreviewThumbnail(c *fiber.Ctx) error { // get current thumbnail - lesson := structs.Lesson{} + lesson := models.Lesson{} - database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).First(&lesson) + if err := database.DB.Model(&models.Lesson{}).Where("id = ?", params.LessonId).First(&lesson); err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } // delete current thumbnail @@ -253,7 +266,9 @@ func UpdateLessonPreviewThumbnail(c *fiber.Ctx) error { utils.CreateFolderStructureIfNotExists(publicPath) - database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).Update("thumbnail_url", databasePath+fileName) + if err := database.DB.Model(&models.Lesson{}).Where("id = ?", params.LessonId).Update("thumbnail_url", databasePath+fileName); err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } return c.SaveFile(fileHeader, publicPath+fileName) } @@ -293,7 +308,9 @@ func UpdateLessonState(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusBadRequest) } - database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).Update("state", body.State) + if err := database.DB.Model(&models.Lesson{}).Where("id = ?", params.LessonId).Update("state", body.State); err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } return c.JSON( fiber.Map{ @@ -341,13 +358,15 @@ func AddLessonContent(c *fiber.Ctx) error { // get last position - var lastContent structs.LessonContent + var lastContent models.LessonContent - database.DB.Select("position").Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId).Order("position desc").First(&lastContent) + if err := database.DB.Select("position").Model(&models.LessonContent{}).Where("lesson_id = ?", params.LessonId).Order("position desc").First(&lastContent); err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } // create new content - content := structs.LessonContent{ + content := models.LessonContent{ Id: uuid.New().String(), LessonId: params.LessonId, Page: 1, @@ -356,7 +375,9 @@ func AddLessonContent(c *fiber.Ctx) error { Data: body.Data, } - database.DB.Create(&content) + if err := database.DB.Create(&content); err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } return c.JSON( structs.AddLessonContentResponse{ @@ -426,9 +447,11 @@ func UploadLessonContentFile(c *fiber.Ctx) error { // get current file - content := structs.LessonContent{} + content := models.LessonContent{} - database.DB.Select("type", "data").Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).First(&content) + if err := database.DB.Select("type", "data").Model(&models.LessonContent{}).Where("id = ?", params.ContentId).First(&content); err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } // delete current image @@ -442,7 +465,9 @@ func UploadLessonContentFile(c *fiber.Ctx) error { utils.CreateFolderStructureIfNotExists(publicPath) - database.DB.Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).Update("data", databasePath+fileName) + if err := database.DB.Model(&models.LessonContent{}).Where("id = ?", params.ContentId).Update("data", databasePath+fileName).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } if err := c.SaveFile(fileHeader, publicPath+fileName); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ @@ -494,7 +519,9 @@ func UpdateLessonContent(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusBadRequest) } - database.DB.Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).Update("data", body.Data) + if err := database.DB.Model(&models.LessonContent{}).Where("id = ?", params.ContentId).Update("data", body.Data).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } return c.JSON( fiber.Map{ @@ -553,8 +580,8 @@ func UpdateLessonContentPosition(c *fiber.Ctx) error { } // 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 { + var currentContent models.LessonContent + if err := tx.Model(&models.LessonContent{}).Where("id = ?", params.ContentId).First(¤tContent).Error; err != nil { tx.Rollback() return c.SendStatus(fiber.StatusNotFound) } @@ -570,7 +597,7 @@ func UpdateLessonContentPosition(c *fiber.Ctx) error { if oldPosition < newPosition { // Items between oldPosition and newPosition need to be shifted down - if err := tx.Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId). + if err := tx.Model(&models.LessonContent{}).Where("lesson_id = ?", params.LessonId). Where("position > ? AND position <= ?", oldPosition, newPosition). Update("position", gorm.Expr("position - 1")).Error; err != nil { tx.Rollback() @@ -578,7 +605,7 @@ func UpdateLessonContentPosition(c *fiber.Ctx) error { } } else { // Items between newPosition and oldPosition need to be shifted up - if err := tx.Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId). + if err := tx.Model(&models.LessonContent{}).Where("lesson_id = ?", params.LessonId). Where("position >= ? AND position < ?", newPosition, oldPosition). Update("position", gorm.Expr("position + 1")).Error; err != nil { tx.Rollback() @@ -587,7 +614,7 @@ func UpdateLessonContentPosition(c *fiber.Ctx) error { } // Update the position of the moved content - if err := tx.Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).Update("position", newPosition).Error; err != nil { + if err := tx.Model(&models.LessonContent{}).Where("id = ?", params.ContentId).Update("position", newPosition).Error; err != nil { tx.Rollback() return c.SendStatus(fiber.StatusInternalServerError) } @@ -640,8 +667,8 @@ func DeleteLessonContent(c *fiber.Ctx) error { } // 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 { + var content models.LessonContent + if err := tx.Model(&models.LessonContent{}).Where("id = ?", params.ContentId).First(&content).Error; err != nil { tx.Rollback() return c.SendStatus(fiber.StatusNotFound) } @@ -653,7 +680,7 @@ func DeleteLessonContent(c *fiber.Ctx) error { } // Shift down the positions of all contents with a higher position - if err := tx.Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId). + if err := tx.Model(&models.LessonContent{}).Where("lesson_id = ?", params.LessonId). Where("position > ?", content.Position). Update("position", gorm.Expr("position - 1")).Error; err != nil { tx.Rollback() diff --git a/routers/router/api/v1/organization/organization.go b/routers/router/api/v1/organization/organization.go index ddbc9c5..3ce7105 100644 --- a/routers/router/api/v1/organization/organization.go +++ b/routers/router/api/v1/organization/organization.go @@ -4,12 +4,15 @@ import ( "encoding/base64" "time" + "git.ex.umbach.dev/Alex/roese-utils/rslogger" "git.ex.umbach.dev/Alex/roese-utils/rsutils" + "git.ex.umbach.dev/LMS/libcore/models" "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" ) @@ -64,23 +67,42 @@ func CreateOrganization(c *fiber.Ctx) error { organizationId := uuid.New().String() userId := uuid.New().String() subdomain := utils.GenerateSubdomain() + // roleId := uuid.New().String() - database.DB.Create(&structs.Organization{ - Id: organizationId, - Subdomain: subdomain, - OwnerUserId: userId, - CompanyName: "Mustermann GmbH", - }) + if err := database.DB.Create(&models.Organization{ + Id: organizationId, + Subdomain: subdomain, + OwnerUserId: userId, + CompanyName: "Mustermann GmbH", + PrimaryColor: "1677ff", // blue + }).Error; err != nil { + logger.AddSystemLog(rslogger.LogTypeError, "Failed to create organization, err: "+err.Error()) + return c.SendStatus(fiber.StatusInternalServerError) + } - database.DB.Create(&structs.User{ + /* + if err := database.DB.Create(&models.Role{ + Id: roleId, + OrganizationId: organizationId, + Master: true, + Name: "Admin", + }).Error; err != nil { + logger.AddSystemLog(rslogger.LogTypeError, "Failed to create role, err: "+err.Error()) + return c.SendStatus(fiber.StatusInternalServerError) + } */ + + if err := database.DB.Create(&models.User{ Id: userId, OrganizationId: organizationId, - Active: true, FirstName: "Max", LastName: "Mustermann", Email: body.Email, Password: string(hashedPassword), - }) + RoleId: utils.RoleAdminId, + }).Error; err != nil { + logger.AddSystemLog(rslogger.LogTypeError, "Failed to create user, err: "+err.Error()) + return c.SendStatus(fiber.StatusInternalServerError) + } session, err := rsutils.GenerateSession() @@ -88,7 +110,7 @@ func CreateOrganization(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusInternalServerError) } - database.DB.Create(&structs.UserSession{ + if err := database.DB.Create(&models.UserSession{ Id: session, OrganizationId: organizationId, Session: uuid.New().String(), @@ -96,7 +118,10 @@ func CreateOrganization(c *fiber.Ctx) error { UserAgent: string(c.Context().UserAgent()), ExpiresAt: utils.GetSessionExpiresAtTime(), LastUsedAt: time.Now(), - }) + }).Error; err != nil { + logger.AddSystemLog(rslogger.LogTypeError, "Failed to create user session, err: "+err.Error()) + return c.SendStatus(fiber.StatusInternalServerError) + } return c.JSON(structs.CreateOrganizationResponse{ OrganizationSubdomain: subdomain, @@ -131,7 +156,7 @@ func IsSubdomainAvailable(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusBadRequest) } - var organization structs.Organization + var organization models.Organization database.DB.Select("Id").Where("subdomain = ?", params.Subdomain).First(&organization) @@ -145,47 +170,3 @@ func IsSubdomainAvailable(c *fiber.Ctx) error { Available: true, }) } - -func UpdateSubdomain(c *fiber.Ctx) error { - // swagger:operation PATCH /organization/subdomain/{subdomain} organization updateSubdomain - // --- - // summary: Update organization subdomain - // consumes: - // - application/json - // produces: - // - application/json - // parameters: - // - name: subdomain - // in: path - // required: true - // type: string - // responses: - // '200': - // description: Subdomain updated successfully - // '400': - // description: Invalid request body - // '500': - // description: Failed to update subdomain - - var params structs.SubdomainParam - - if err := rsutils.ParamsParserHelper(c, ¶ms); err != nil { - return c.SendStatus(fiber.StatusBadRequest) - } - - organization := structs.Organization{ - Id: c.Locals("organizationId").(string), - } - - database.DB.Select("subdomain").Model(organization).First(&organization) - - if organization.Subdomain == "" { - return c.SendStatus(fiber.StatusBadRequest) - } - - database.DB.Model(&organization).Update("subdomain", params.Subdomain) - - return c.JSON(fiber.Map{ - "status": "success", - }) -} diff --git a/routers/router/api/v1/organization/roles.go b/routers/router/api/v1/organization/roles.go new file mode 100644 index 0000000..d1d41c4 --- /dev/null +++ b/routers/router/api/v1/organization/roles.go @@ -0,0 +1,123 @@ +package organization + +import ( + "git.ex.umbach.dev/LMS/libcore/models" + "github.com/gofiber/fiber/v2" + "lms.de/backend/modules/database" + "lms.de/backend/modules/structs" + "lms.de/backend/modules/utils" +) + +func GetRoles(c *fiber.Ctx) error { + // swagger:operation GET /organization/roles organization getRoles + // --- + // summary: Get roles + // produces: + // - application/json + // responses: + // '200': + // description: Roles + // schema: + // "$ref": "#/definitions/RolesResponse" + // '500': + // description: Failed to get roles + + /* + var roles []models.Role + + if err := database.DB.Model(&models.Role{}).Where("organization_id = ?", c.Locals("organizationId").(string)).Find(&roles).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + if len(roles) == 0 { + return c.SendStatus(fiber.StatusInternalServerError) + } + + for _, role := range roles { + var rolePermissions []uint16 + + if err := database.DB.Model(&models.RolePermission{}).Where("role_id = ?", role.Id).Pluck("permission", &rolePermissions).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + var users []structs.HelperRoleUser + + if err := database.DB.Model(&models.User{}).Where("role_id = ?", role.Id).Where("organization_id = ?", c.Locals("organizationId").(string)).Find(&users).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + rolesResponse.Roles = append(rolesResponse.Roles, structs.HelperRole{ + Id: role.Id, + Name: role.Name, + Master: role.Master, + Permissions: rolePermissions, + Users: users, + }) + } */ + + var rolesResponse structs.RolesResponse + + // order is random so we need to define the order + keys := []string{utils.RoleAdminId, utils.RoleModeratorId, utils.RoleUserId} + + for _, key := range keys { + role := utils.Roles[key] + + var users []structs.HelperRoleUser + + if err := database.DB.Model(&models.User{}).Where("role_id = ?", key).Where("organization_id = ?", c.Locals("organizationId").(string)).Find(&users).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + rolesResponse.Roles = append(rolesResponse.Roles, structs.HelperRole{ + Id: key, + Permissions: role, + Users: users, + }) + } + + return c.JSON(rolesResponse) +} + +/* +func CreateRole(c *fiber.Ctx) error { + // swagger:operation POST /organization/roles organization createRole + // --- + // summary: Create role + // produces: + // - application/json + // parameters: + // - name: name + // in: body + // description: Role name + // required: true + // schema: + // "$ref": "#/definitions/CreateRoleRequest" + // responses: + // '200': + // description: Role created + // '400': + // description: Invalid input + // '500': + // description: Failed to create role + + var body structs.CreateRoleRequest + + if err := c.BodyParser(&body); err != nil { + fmt.Print(err) + return c.SendStatus(fiber.StatusBadRequest) + } + + role := models.Role{ + Id: uuid.New().String(), + OrganizationId: c.Locals("organizationId").(string), + Name: body.Name, + } + + if err := database.DB.Create(&role).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + return c.SendStatus(fiber.StatusOK) +} +*/ diff --git a/routers/router/api/v1/organization/settings.go b/routers/router/api/v1/organization/settings.go index c68b173..4082470 100644 --- a/routers/router/api/v1/organization/settings.go +++ b/routers/router/api/v1/organization/settings.go @@ -1,13 +1,19 @@ package organization import ( + "errors" "strings" + "time" + "git.ex.umbach.dev/Alex/roese-utils/rsutils" + "git.ex.umbach.dev/LMS/libcore/models" "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" + "lms.de/backend/socketclients" ) func GetOrganizationSettings(c *fiber.Ctx) error { @@ -30,7 +36,12 @@ func GetOrganizationSettings(c *fiber.Ctx) error { var organizationSettings structs.GetOrganizationSettingsResponse - database.DB.Model(&structs.Organization{}).Select("subdomain", "company_name", "primary_color", "logo_url", "banner_url").First(&organizationSettings) + if err := database.DB.Model(&models.Organization{}). + Select("subdomain", "company_name", "primary_color", "logo_url", "banner_url"). + Where("id = ?", c.Locals("organizationId").(string)). + First(&organizationSettings).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } return c.JSON(organizationSettings) } @@ -62,9 +73,18 @@ func UpdateOrganizationSettings(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusBadRequest) } - database.DB.Model(&structs.Organization{ - Id: c.Locals("organizationId").(string), - }).Updates(organizationSettings) + if err := database.DB.Model(&models.Organization{}).Where("id = ?", c.Locals("organizationId").(string)).Updates(organizationSettings).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + socketclients.BroadcastMessageExceptBrowserTabSession( + c.Locals("organizationId").(string), + c.Locals("browserTabSession").(string), + structs.SendSocketMessage{ + Cmd: utils.SendCmdSettingsUpdated, + Body: organizationSettings, + }, + ) return c.SendStatus(fiber.StatusOK) } @@ -120,7 +140,7 @@ func UpdateOrganizationFile(c *fiber.Ctx) error { // get current file - organization := structs.Organization{} + organization := models.Organization{} selectField := "logo_url" @@ -128,9 +148,11 @@ func UpdateOrganizationFile(c *fiber.Ctx) error { selectField = "banner_url" } - database.DB.Model(&structs.Organization{ + if err := database.DB.Model(&models.Organization{ Id: c.Locals("organizationId").(string), - }).Select(selectField).First(&organization) + }).Select(selectField).First(&organization).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } // delete current file @@ -148,7 +170,7 @@ func UpdateOrganizationFile(c *fiber.Ctx) error { utils.CreateFolderStructureIfNotExists(publicPath) - update := structs.Organization{} + update := models.Organization{} if params.Type == "logo" { update.LogoUrl = databasePath + fileName @@ -156,9 +178,9 @@ func UpdateOrganizationFile(c *fiber.Ctx) error { update.BannerUrl = databasePath + fileName } - database.DB.Model(&structs.Organization{ - Id: c.Locals("organizationId").(string), - }).Updates(update) + if err := database.DB.Model(&models.Organization{}).Where("id = ?", c.Locals("organizationId").(string)).Updates(update).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } if err := c.SaveFile(fileHeader, publicPath+fileName); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ @@ -166,7 +188,85 @@ func UpdateOrganizationFile(c *fiber.Ctx) error { }) } + cmdId := utils.SendCmdSettingsUpdatedLogo + + if params.Type == "banner" { + cmdId = utils.SendCmdSettingsUpdatedBanner + } + + newPath := databasePath + fileName + + socketclients.BroadcastMessageExceptBrowserTabSession( + c.Locals("organizationId").(string), + c.Locals("browserTabSession").(string), + structs.SendSocketMessage{ + Cmd: cmdId, + Body: newPath, + }, + ) + return c.JSON(fiber.Map{ - "Data": databasePath + fileName, + "Data": newPath, + }) +} + +func UpdateSubdomain(c *fiber.Ctx) error { + // swagger:operation PATCH /organization/subdomain/{subdomain} organization updateSubdomain + // --- + // summary: Update organization subdomain + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: subdomain + // in: path + // required: true + // type: string + // responses: + // '200': + // description: Subdomain updated successfully + // '400': + // description: Invalid request body + // '500': + // description: Failed to update subdomain + + var params structs.SubdomainParam + + if err := rsutils.ParamsParserHelper(c, ¶ms); err != nil { + return c.SendStatus(fiber.StatusBadRequest) + } + + var organization models.Organization + + if err := database.DB.Select("subdomain").Model(organization).Where("id = ?", c.Locals("organizationId").(string)).First(&organization).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return c.SendStatus(fiber.StatusNotFound) + } + } + if organization.Subdomain == "" { + return c.SendStatus(fiber.StatusBadRequest) + } + + if err := database.DB.Model(&organization).Where("id = ?", c.Locals("organizationId")).Update("subdomain", params.Subdomain).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + orgId := c.Locals("organizationId").(string) + + // send broadcast with delay + go func() { + time.Sleep(1 * time.Second) + + socketclients.BroadcastMessage(orgId, + structs.SendSocketMessage{ + Cmd: utils.SendCmdSettingsUpdatedSubdomain, + Body: params.Subdomain, + }, + ) + }() + + return c.JSON(fiber.Map{ + "status": "success", }) } diff --git a/routers/router/api/v1/organization/team.go b/routers/router/api/v1/organization/team.go index 125b5f2..0784ce0 100644 --- a/routers/router/api/v1/organization/team.go +++ b/routers/router/api/v1/organization/team.go @@ -1,9 +1,18 @@ package organization import ( + "encoding/base64" + + "git.ex.umbach.dev/Alex/roese-utils/rsutils" + "git.ex.umbach.dev/LMS/libcore/models" "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" + "lms.de/backend/socketclients" ) func GetTeamMembers(c *fiber.Ctx) error { @@ -28,7 +37,99 @@ func GetTeamMembers(c *fiber.Ctx) error { var users []structs.TeamMember - database.DB.Model(&structs.User{}).Where("organization_id = ?", c.Locals("organizationId")).Find(&users) + if err := database.DB.Model(&models.User{}).Where("organization_id = ?", c.Locals("organizationId")).Find(&users).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } return c.JSON(users) } + +func CreateTeamMember(c *fiber.Ctx) error { + // swagger:operation POST /organization/team/members organization createTeamMember + // --- + // summary: Create team member + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateTeamMemberRequest" + // responses: + // '200': + // description: Team member created successfully + // '400': + // description: Invalid request body + // '500': + // description: Failed to create team member + + var body structs.CreateTeamMemberRequest + + if err := rsutils.BodyParserHelper(c, &body); err != nil { + return c.SendStatus(fiber.StatusBadRequest) + } + + user := models.User{ + Id: uuid.New().String(), + FirstName: body.FirstName, + LastName: body.LastName, + Email: body.Email, + OrganizationId: c.Locals("organizationId").(string), + RoleId: body.RoleId, + } + + 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) + } + + // Hash password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(body.Password), bcrypt.DefaultCost) + + if err != nil { + log.Error().Msg("Failed to hash password, err: " + err.Error()) + return c.SendStatus(fiber.StatusInternalServerError) + } + + user.Password = string(hashedPassword) + + // Create user + + if err := database.DB.Create(&user).Error; err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + socketclients.BroadcastMessageToTopicExceptBrowserTabSession( + c.Locals("organizationId").(string), + utils.SubscribedTopicTeam, + c.Locals("browserTabSession").(string), + structs.SendSocketMessage{ + Cmd: utils.SendCmdTeamAddedMember, + Body: struct { + Id string + FirstName string + LastName string + Email string + RoleId string + }{ + Id: user.Id, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + RoleId: user.RoleId, + }, + }, + ) + + return c.JSON(fiber.Map{ + "message": "Team member created successfully", + }) +} diff --git a/routers/router/api/v1/user/auth.go b/routers/router/api/v1/user/auth.go index 0e73690..5ed6fd0 100644 --- a/routers/router/api/v1/user/auth.go +++ b/routers/router/api/v1/user/auth.go @@ -5,6 +5,7 @@ import ( "git.ex.umbach.dev/Alex/roese-utils/rslogger" "git.ex.umbach.dev/Alex/roese-utils/rsutils" + "git.ex.umbach.dev/LMS/libcore/models" "github.com/gofiber/fiber/v2" "github.com/google/uuid" "github.com/rs/zerolog/log" @@ -57,11 +58,14 @@ func UserLogin(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusBadRequest) } - var user structs.User + var user models.User organizationId := c.Locals("organizationId").(string) - database.DB.Select("id", "active", "password").First(&user, "email = ? AND organization_id = ?", body.Email, organizationId) + if err := database.DB.Select("id", "disabled", "password").First(&user, "email = ? AND organization_id = ?", body.Email, organizationId).Error; err != nil { + logger.AddSystemLog(rslogger.LogTypeError, "Failed to find user with email %s", body.Email) + return c.SendStatus(fiber.StatusBadRequest) + } if user.Id == "" { log.Error().Msg("User not found") @@ -73,7 +77,7 @@ func UserLogin(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusBadRequest) } - if !user.Active { + if user.Disabled { return c.SendStatus(fiber.StatusUnauthorized) } @@ -83,13 +87,16 @@ func UserLogin(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusInternalServerError) } - database.DB.Create(&structs.UserSession{ + if err := database.DB.Create(&models.UserSession{ Id: uuid.New().String(), OrganizationId: organizationId, Session: session, UserId: user.Id, UserAgent: string(c.Context().UserAgent()), - ExpiresAt: utils.GetSessionExpiresAtTime()}) + ExpiresAt: utils.GetSessionExpiresAtTime()}).Error; err != nil { + logger.AddSystemLog(rslogger.LogTypeError, "Failed to create user session, err: "+err.Error()) + return c.SendStatus(fiber.StatusInternalServerError) + } logger.AddSystemLog(rslogger.LogTypeInfo, "User %s has logged in", user.Id) diff --git a/routers/router/router.go b/routers/router/router.go index 701b5e2..1d3100d 100644 --- a/routers/router/router.go +++ b/routers/router/router.go @@ -3,10 +3,10 @@ package router import ( "strings" + "git.ex.umbach.dev/LMS/libcore/models" "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" myapp "lms.de/backend/routers/router/api/v1/app" "lms.de/backend/routers/router/api/v1/lessons" @@ -22,11 +22,14 @@ func SetupRoutes(app *fiber.App) { o := v1.Group("/organization") o.Post("/", organization.CreateOrganization) o.Get("/team/members", handleOrganizationSubdomain, requestAccessValidation, organization.GetTeamMembers) + o.Post("/team/members", handleOrganizationSubdomain, requestAccessValidation, organization.CreateTeamMember) o.Get("/settings", handleOrganizationSubdomain, requestAccessValidation, organization.GetOrganizationSettings) o.Patch("/settings", handleOrganizationSubdomain, requestAccessValidation, organization.UpdateOrganizationSettings) o.Post("/file/:type", handleOrganizationSubdomain, requestAccessValidation, organization.UpdateOrganizationFile) o.Get("/subdomain/:subdomain", organization.IsSubdomainAvailable) o.Patch("/subdomain/:subdomain", handleOrganizationSubdomain, requestAccessValidation, organization.UpdateSubdomain) + o.Get("/roles", handleOrganizationSubdomain, requestAccessValidation, organization.GetRoles) + // o.Post("/roles", handleOrganizationSubdomain, requestAccessValidation, organization.CreateRole) u := v1.Group("/user") u.Post("/auth/login", handleOrganizationSubdomain, user.UserLogin) @@ -55,7 +58,7 @@ func userSessionValidation(c *fiber.Ctx) error { return fiber.ErrUnauthorized } - var userSession structs.UserSession + var userSession models.UserSession database.DB.Select("session", "user_id").First(&userSession, "session = ? AND organization_id = ?", xAuthorization, c.Locals("organizationId")) @@ -70,6 +73,14 @@ func userSessionValidation(c *fiber.Ctx) error { } func requestAccessValidation(c *fiber.Ctx) error { + // browser tab session - needed for websocket + + browserTabSession := utils.GetBrowserTabSessionHeader(c) + + if len(browserTabSession) == utils.LenHeaderBrowserTabSession { + c.Locals("browserTabSession", browserTabSession) + } + // user session xAuthorization := utils.GetXAuhorizationHeader(c) @@ -100,7 +111,7 @@ func handleOrganizationSubdomain(c *fiber.Ctx) error { subdomain := parts[0] // get organization id by subdomain from database - organization := structs.Organization{} + organization := models.Organization{} database.DB.Select("id").First(&organization, "subdomain = ?", subdomain) diff --git a/socketclients/socketclients.go b/socketclients/socketclients.go new file mode 100644 index 0000000..9e4562a --- /dev/null +++ b/socketclients/socketclients.go @@ -0,0 +1,44 @@ +package socketclients + +import ( + "strings" + + "lms.de/backend/modules/cache" + "lms.de/backend/modules/structs" +) + +func BroadcastMessage(organizationId string, sendSocketMessage structs.SendSocketMessage) { + for _, client := range cache.GetSocketClients() { + if client.OrganizationId == organizationId { + client.SendMessage(sendSocketMessage) + } + } +} + +func BroadcastMessageToTopic(organizationId string, topic string, sendSocketMessage structs.SendSocketMessage) { + for _, client := range cache.GetSocketClients() { + if hasClientSubscribedToTopic(topic, client.SubscribedTopic) { + client.SendMessage(sendSocketMessage) + } + } +} + +func BroadcastMessageExceptBrowserTabSession(organizationId string, browserTabSession string, sendSocketMessage structs.SendSocketMessage) { + for _, client := range cache.GetSocketClients() { + if client.OrganizationId == organizationId && client.BrowserTabSession != browserTabSession { + client.SendMessage(sendSocketMessage) + } + } +} + +func BroadcastMessageToTopicExceptBrowserTabSession(organizationId string, topic string, browserTabSession string, sendSocketMessage structs.SendSocketMessage) { + for _, client := range cache.GetSocketClients() { + if hasClientSubscribedToTopic(topic, client.SubscribedTopic) && client.BrowserTabSession != browserTabSession && client.OrganizationId == organizationId { + client.SendMessage(sendSocketMessage) + } + } +} + +func hasClientSubscribedToTopic(topic string, clientTopic string) bool { + return clientTopic == topic || strings.HasPrefix(clientTopic, topic) +} diff --git a/socketserver/hub.go b/socketserver/hub.go index 5f7dc39..3d3bd0f 100644 --- a/socketserver/hub.go +++ b/socketserver/hub.go @@ -7,6 +7,7 @@ import ( "time" "git.ex.umbach.dev/Alex/roese-utils/rslogger" + "git.ex.umbach.dev/LMS/libcore/models" "github.com/gofiber/websocket/v2" "github.com/rs/zerolog/log" "lms.de/backend/modules/cache" @@ -37,22 +38,23 @@ func RunHub() { newSocketClient.SessionId = sessionId newSocketClient.BrowserTabSession = browserTabSession newSocketClient.UserId = userId + newSocketClient.OrganizationId = fmt.Sprintf("%v", newSocketClient.Conn.Locals("organizationId")) cache.AddSocketClient(newSocketClient) // check that user session is not expired - var userSession structs.UserSession + var userSession models.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) + database.DB.Delete(&models.UserSession{}, "session = ?", sessionId) continue } // update session last used time - database.DB.Model(&structs.UserSession{}).Where("session = ?", sessionId).Updates(structs.UserSession{ + database.DB.Model(&models.UserSession{}).Where("session = ?", sessionId).Updates(models.UserSession{ LastUsedAt: time.Now(), ExpiresAt: utils.GetSessionExpiresAtTime(), }) @@ -72,7 +74,9 @@ func RunHub() { log.Debug().Msgf("Received message: %v %v", receivedMessage, receivedMessage.Cmd) switch receivedMessage.Cmd { - case 1: + case utils.ReceivedCmdSubscribeToTopic: + cache.SubscribeSocketClientToTopic(receivedMessage.Body["browserTabSession"].(string), receivedMessage.Body["topic"].(string)) + case 2: break default: log.Error().Msgf("Received unknown message: %v", receivedMessage) @@ -85,7 +89,7 @@ func RunHub() { userId := connection.Locals("userId").(string) // sessionId := connection.Locals("sessionId").(string) - database.DB.Model(&structs.User{}).Where("id = ?", userId).Updates(structs.User{ + database.DB.Model(&models.User{}).Where("id = ?", userId).Updates(models.User{ LastOnlineAt: time.Now(), })