From 1232a909c1b5bf59338119482fdfb23c2e947c8f Mon Sep 17 00:00:00 2001 From: alex Date: Sun, 8 Sep 2024 00:32:01 +0200 Subject: [PATCH] websocket --- go.mod | 1 + go.sum | 8 ++ main.go | 8 +- modules/cache/permissions.go | 19 +++ modules/database/database.go | 24 ++-- modules/permissions/permissions.go | 15 +++ modules/structs/lessons.go | 22 ---- modules/structs/organizations.go | 15 --- modules/structs/questions.go | 37 ------ modules/structs/roles.go | 41 ++++-- modules/structs/socket.go | 1 + modules/structs/users.go | 38 ++---- modules/utils/globals.go | 70 +++++++++- modules/utils/utils.go | 13 +- public/demo/logo.png | Bin 0 -> 7798 bytes routers/router/api/v1/app/app.go | 11 +- routers/router/api/v1/lessons/lessons.go | 79 +++++++---- .../api/v1/organization/organization.go | 93 ++++++------- routers/router/api/v1/organization/roles.go | 123 +++++++++++++++++ .../router/api/v1/organization/settings.go | 124 ++++++++++++++++-- routers/router/api/v1/organization/team.go | 103 ++++++++++++++- routers/router/api/v1/user/auth.go | 17 ++- routers/router/router.go | 17 ++- socketclients/socketclients.go | 44 +++++++ socketserver/hub.go | 14 +- 25 files changed, 688 insertions(+), 249 deletions(-) create mode 100644 modules/cache/permissions.go create mode 100644 modules/permissions/permissions.go create mode 100644 public/demo/logo.png create mode 100644 routers/router/api/v1/organization/roles.go create mode 100644 socketclients/socketclients.go 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 0000000000000000000000000000000000000000..3a1c4d3a9a89c7bd431cdd4f05d42c28bb3e6935 GIT binary patch literal 7798 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 IM6N<$g77pH-2eap literal 0 HcmV?d00001 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(), })