diff --git a/modules/structs/organizations.go b/modules/structs/organizations.go index e5806eb..6e13523 100644 --- a/modules/structs/organizations.go +++ b/modules/structs/organizations.go @@ -27,6 +27,7 @@ type CreateOrganizationResponse struct { Session string } +// swagger:model GetOrganizationSettingsResponse type GetOrganizationSettingsResponse struct { Subdomain string CompanyName string @@ -35,7 +36,23 @@ type GetOrganizationSettingsResponse struct { BannerUrl string } +// swagger:model UpdateOrganizationSettingsRequest type UpdateOrganizationSettingsRequest struct { CompanyName string PrimaryColor string } + +// swagger:model UpdateOrganizationFileParam +type UpdateOrganizationFileParam struct { + Type string +} + +// swagger:model IsSubdomainAvailableResponse +type IsSubdomainAvailableResponse struct { + Available bool +} + +// swagger:model SubdomainParam +type SubdomainParam struct { + Subdomain string +} diff --git a/modules/utils/utils.go b/modules/utils/utils.go index af04e80..687d0e3 100644 --- a/modules/utils/utils.go +++ b/modules/utils/utils.go @@ -110,7 +110,11 @@ func IsFileTypeAllowed(contentType string, allowedContentTypes []string) bool { } func DeleteFile(filePath string) { - os.Remove(filePath) + err := os.Remove(config.Cfg.FolderPaths.PublicStatic + filePath) + + if err != nil { + log.Error().Msgf("Failed to delete file: %v", err) + } } func CreateFolderStructureIfNotExists(folderPath string) { @@ -123,6 +127,13 @@ func CreateFolderStructureIfNotExists(folderPath string) { // GetFullImagePath returns the database path and the public path for the image func GetFullImagePath(organizationId string, lessonId string) (databasePath string, publicPath string) { + if lessonId == "" { + return fmt.Sprintf( + "o/%s/", organizationId), + fmt.Sprintf( + "%s/o/%s/", config.Cfg.FolderPaths.PublicStatic, organizationId) + } + return fmt.Sprintf( "o/%s/l/%s/", organizationId, lessonId), fmt.Sprintf( diff --git a/routers/router/api/v1/lessons/lessons.go b/routers/router/api/v1/lessons/lessons.go index 44e8a51..bfea37f 100644 --- a/routers/router/api/v1/lessons/lessons.go +++ b/routers/router/api/v1/lessons/lessons.go @@ -197,7 +197,7 @@ func UpdateLessonPreviewThumbnail(c *fiber.Ctx) error { // --- // summary: Update lesson preview thumbnail. // consumes: - // - application/json + // - multipart/form-data // produces: // - application/json // parameters: diff --git a/routers/router/api/v1/organization/organization.go b/routers/router/api/v1/organization/organization.go index 5165c23..ddbc9c5 100644 --- a/routers/router/api/v1/organization/organization.go +++ b/routers/router/api/v1/organization/organization.go @@ -103,3 +103,89 @@ func CreateOrganization(c *fiber.Ctx) error { Session: session, }) } + +func IsSubdomainAvailable(c *fiber.Ctx) error { + // swagger:operation GET /organization/subdomain/{subdomain} organization isSubdomainAvailable + // --- + // summary: Check if subdomain is available + // produces: + // - application/json + // parameters: + // - name: subdomain + // in: path + // required: true + // type: string + // responses: + // '200': + // description: Subdomain is available + // schema: + // "$ref": "#/definitions/IsSubdomainAvailableResponse" + // '400': + // description: Invalid subdomain + // '500': + // description: Failed to check subdomain + + var params structs.SubdomainParam + + if err := rsutils.ParamsParserHelper(c, ¶ms); err != nil { + return c.SendStatus(fiber.StatusBadRequest) + } + + var organization structs.Organization + + database.DB.Select("Id").Where("subdomain = ?", params.Subdomain).First(&organization) + + if organization.Id != "" { + return c.JSON(structs.IsSubdomainAvailableResponse{ + Available: false, + }) + } + + return c.JSON(structs.IsSubdomainAvailableResponse{ + 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/settings.go b/routers/router/api/v1/organization/settings.go index 8bc60d1..c68b173 100644 --- a/routers/router/api/v1/organization/settings.go +++ b/routers/router/api/v1/organization/settings.go @@ -1,9 +1,13 @@ package organization import ( + "strings" + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" "lms.de/backend/modules/database" "lms.de/backend/modules/structs" + "lms.de/backend/modules/utils" ) func GetOrganizationSettings(c *fiber.Ctx) error { @@ -64,3 +68,105 @@ func UpdateOrganizationSettings(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) } + +func UpdateOrganizationFile(c *fiber.Ctx) error { + // swagger:operation POST /organization/file/{type} organization updateOrganizationFiles + // --- + // summary: Update files + // consumes: + // - multipart/form-data + // produces: + // - application/json + // parameters: + // - name: logo + // in: formData + // type: file + // required: false + // - name: banner + // in: formData + // type: file + // required: false + // responses: + // '200': + // description: Files updated successfully + // '400': + // description: Invalid request body + // '500': + // description: Failed to update files + + var params structs.UpdateOrganizationFileParam + + if err := c.ParamsParser(¶ms); err != nil { + return c.SendStatus(fiber.StatusBadRequest) + } + + fileHeader, err := c.FormFile("file") + + if err != nil { + return c.SendStatus(fiber.StatusBadRequest) + } + + if params.Type == "logo" || params.Type == "banner" { + if fileHeader.Size > utils.MaxImageSize { + return c.SendStatus(fiber.StatusBadRequest) + } + + if !utils.IsFileTypeAllowed(fileHeader.Header.Get("Content-Type"), utils.AcceptedImageFileTypes) { + return c.SendStatus(fiber.StatusBadRequest) + } + } else { + return c.SendStatus(fiber.StatusBadRequest) + } + + // get current file + + organization := structs.Organization{} + + selectField := "logo_url" + + if params.Type == "banner" { + selectField = "banner_url" + } + + database.DB.Model(&structs.Organization{ + Id: c.Locals("organizationId").(string), + }).Select(selectField).First(&organization) + + // delete current file + + if organization.LogoUrl != "" { + utils.DeleteFile(organization.LogoUrl) + } + + if organization.BannerUrl != "" { + utils.DeleteFile(organization.BannerUrl) + } + + fileName := uuid.New().String() + "." + strings.Split(fileHeader.Header["Content-Type"][0], "/")[1] + + databasePath, publicPath := utils.GetFullImagePath(c.Locals("organizationId").(string), "") + + utils.CreateFolderStructureIfNotExists(publicPath) + + update := structs.Organization{} + + if params.Type == "logo" { + update.LogoUrl = databasePath + fileName + } else { + update.BannerUrl = databasePath + fileName + } + + database.DB.Model(&structs.Organization{ + Id: c.Locals("organizationId").(string), + }).Updates(update) + + if err := c.SaveFile(fileHeader, publicPath+fileName); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to save file", + }) + } + + return c.JSON(fiber.Map{ + "Data": databasePath + fileName, + }) +} diff --git a/routers/router/router.go b/routers/router/router.go index 059a764..701b5e2 100644 --- a/routers/router/router.go +++ b/routers/router/router.go @@ -24,6 +24,9 @@ func SetupRoutes(app *fiber.App) { o.Get("/team/members", handleOrganizationSubdomain, requestAccessValidation, organization.GetTeamMembers) 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) u := v1.Group("/user") u.Post("/auth/login", handleOrganizationSubdomain, user.UserLogin)