diff --git a/app/database/entity/clients.entity.go b/app/database/entity/clients.entity.go index 2c8bc9f..20d76da 100644 --- a/app/database/entity/clients.entity.go +++ b/app/database/entity/clients.entity.go @@ -1,8 +1,9 @@ package entity import ( - "github.com/google/uuid" "time" + + "github.com/google/uuid" ) type Clients struct { @@ -13,14 +14,21 @@ type Clients struct { ParentClientId *uuid.UUID `json:"parent_client_id" gorm:"type:UUID;index"` ParentClient *Clients `json:"parent_client,omitempty" gorm:"foreignKey:ParentClientId;references:ID"` SubClients []Clients `json:"sub_clients,omitempty" gorm:"foreignKey:ParentClientId;references:ID"` - + + // Additional tenant information fields + LogoUrl *string `json:"logo_url" gorm:"type:varchar"` // Logo tenant URL + LogoImagePath *string `json:"logo_image_path" gorm:"type:varchar"` // Logo image path in MinIO + Address *string `json:"address" gorm:"type:text"` // Alamat + PhoneNumber *string `json:"phone_number" gorm:"type:varchar"` // Nomor telepon + Website *string `json:"website" gorm:"type:varchar"` // Website resmi + // Metadata - Settings *string `json:"settings" gorm:"type:jsonb"` // JSON for custom settings - MaxUsers *int `json:"max_users" gorm:"type:int4"` // Limit for sub clients - MaxStorage *int64 `json:"max_storage" gorm:"type:int8"` // In bytes - - CreatedById *uint `json:"created_by_id" gorm:"type:int4"` - IsActive *bool `json:"is_active" gorm:"type:bool;default:true"` - CreatedAt time.Time `json:"created_at" gorm:"default:now()"` - UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` + Settings *string `json:"settings" gorm:"type:jsonb"` // JSON for custom settings + MaxUsers *int `json:"max_users" gorm:"type:int4"` // Limit for sub clients + MaxStorage *int64 `json:"max_storage" gorm:"type:int8"` // In bytes + + CreatedById *uint `json:"created_by_id" gorm:"type:int4"` + IsActive *bool `json:"is_active" gorm:"type:bool;default:true"` + CreatedAt time.Time `json:"created_at" gorm:"default:now()"` + UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` } diff --git a/app/module/articles/mapper/articles.mapper.go b/app/module/articles/mapper/articles.mapper.go index 8a7350c..048f104 100644 --- a/app/module/articles/mapper/articles.mapper.go +++ b/app/module/articles/mapper/articles.mapper.go @@ -1,8 +1,6 @@ package mapper import ( - "github.com/google/uuid" - "github.com/rs/zerolog" "netidhub-saas-be/app/database/entity" articleCategoriesMapper "netidhub-saas-be/app/module/article_categories/mapper" articleCategoriesRepository "netidhub-saas-be/app/module/article_categories/repository" @@ -12,7 +10,11 @@ import ( articleFilesRepository "netidhub-saas-be/app/module/article_files/repository" articleFilesResponse "netidhub-saas-be/app/module/article_files/response" res "netidhub-saas-be/app/module/articles/response" + clientsRepository "netidhub-saas-be/app/module/clients/repository" usersRepository "netidhub-saas-be/app/module/users/repository" + + "github.com/google/uuid" + "github.com/rs/zerolog" ) func ArticlesResponseMapper( @@ -24,8 +26,13 @@ func ArticlesResponseMapper( articleCategoryDetailsRepo articleCategoryDetailsRepository.ArticleCategoryDetailsRepository, articleFilesRepo articleFilesRepository.ArticleFilesRepository, usersRepo usersRepository.UsersRepository, + clientsRepo clientsRepository.ClientsRepository, ) (articlesRes *res.ArticlesResponse) { + if articlesReq == nil { + return nil + } + createdByName := "" if articlesReq.CreatedById != nil { findUser, _ := usersRepo.FindOne(clientId, *articlesReq.CreatedById) @@ -34,57 +41,66 @@ func ArticlesResponseMapper( } } + clientName := "" + if articlesReq.ClientId != nil { + findClient, _ := clientsRepo.FindOneByClientId(articlesReq.ClientId) + if findClient != nil { + clientName = findClient.Name + } + } + categoryName := "" articleCategories, _ := articleCategoryDetailsRepo.FindByArticleId(articlesReq.ID) var articleCategoriesArr []*articleCategoriesResponse.ArticleCategoriesResponse - if articleCategories != nil && len(articleCategories) > 0 { + if len(articleCategories) > 0 { for _, result := range articleCategories { - articleCategoriesArr = append(articleCategoriesArr, articleCategoriesMapper.ArticleCategoriesResponseMapper(result.Category, host)) + if result.Category != nil { + articleCategoriesArr = append(articleCategoriesArr, articleCategoriesMapper.ArticleCategoriesResponseMapper(result.Category, host)) + } } log.Info().Interface("articleCategoriesArr", articleCategoriesArr).Msg("") } articleFiles, _ := articleFilesRepo.FindByArticle(clientId, articlesReq.ID) var articleFilesArr []*articleFilesResponse.ArticleFilesResponse - if articleFiles != nil && len(articleFiles) > 0 { + if len(articleFiles) > 0 { for _, result := range articleFiles { articleFilesArr = append(articleFilesArr, articleFilesMapper.ArticleFilesResponseMapper(result, host)) } } - if articlesReq != nil { - articlesRes = &res.ArticlesResponse{ - ID: articlesReq.ID, - Title: articlesReq.Title, - Slug: articlesReq.Slug, - Description: articlesReq.Description, - HtmlDescription: articlesReq.HtmlDescription, - TypeId: articlesReq.TypeId, - Tags: articlesReq.Tags, - CategoryId: articlesReq.CategoryId, - AiArticleId: articlesReq.AiArticleId, - CategoryName: categoryName, - PageUrl: articlesReq.PageUrl, - CreatedById: articlesReq.CreatedById, - CreatedByName: &createdByName, - ShareCount: articlesReq.ShareCount, - ViewCount: articlesReq.ViewCount, - CommentCount: articlesReq.CommentCount, - OldId: articlesReq.OldId, - StatusId: articlesReq.StatusId, - IsBanner: articlesReq.IsBanner, - IsPublish: articlesReq.IsPublish, - PublishedAt: articlesReq.PublishedAt, - IsActive: articlesReq.IsActive, - CreatedAt: articlesReq.CreatedAt, - UpdatedAt: articlesReq.UpdatedAt, - ArticleFiles: articleFilesArr, - ArticleCategories: articleCategoriesArr, - } + articlesRes = &res.ArticlesResponse{ + ID: articlesReq.ID, + Title: articlesReq.Title, + Slug: articlesReq.Slug, + Description: articlesReq.Description, + HtmlDescription: articlesReq.HtmlDescription, + TypeId: articlesReq.TypeId, + Tags: articlesReq.Tags, + CategoryId: articlesReq.CategoryId, + AiArticleId: articlesReq.AiArticleId, + CategoryName: categoryName, + PageUrl: articlesReq.PageUrl, + CreatedById: articlesReq.CreatedById, + CreatedByName: &createdByName, + ClientName: &clientName, + ShareCount: articlesReq.ShareCount, + ViewCount: articlesReq.ViewCount, + CommentCount: articlesReq.CommentCount, + OldId: articlesReq.OldId, + StatusId: articlesReq.StatusId, + IsBanner: articlesReq.IsBanner, + IsPublish: articlesReq.IsPublish, + PublishedAt: articlesReq.PublishedAt, + IsActive: articlesReq.IsActive, + CreatedAt: articlesReq.CreatedAt, + UpdatedAt: articlesReq.UpdatedAt, + ArticleFiles: articleFilesArr, + ArticleCategories: articleCategoriesArr, + } - if articlesReq.ThumbnailName != nil { - articlesRes.ThumbnailUrl = host + "/articles/thumbnail/viewer/" + *articlesReq.ThumbnailName - } + if articlesReq.ThumbnailName != nil { + articlesRes.ThumbnailUrl = host + "/articles/thumbnail/viewer/" + *articlesReq.ThumbnailName } return articlesRes diff --git a/app/module/articles/response/articles.response.go b/app/module/articles/response/articles.response.go index 6757318..255dc55 100644 --- a/app/module/articles/response/articles.response.go +++ b/app/module/articles/response/articles.response.go @@ -20,6 +20,7 @@ type ArticlesResponse struct { PageUrl *string `json:"pageUrl"` CreatedById *uint `json:"createdById"` CreatedByName *string `json:"createdByName"` + ClientName *string `json:"clientName"` ShareCount *int `json:"shareCount"` ViewCount *int `json:"viewCount"` CommentCount *int `json:"commentCount"` diff --git a/app/module/articles/service/articles.service.go b/app/module/articles/service/articles.service.go index 3af222c..cc3b76d 100644 --- a/app/module/articles/service/articles.service.go +++ b/app/module/articles/service/articles.service.go @@ -22,6 +22,7 @@ import ( "netidhub-saas-be/app/module/articles/repository" "netidhub-saas-be/app/module/articles/request" "netidhub-saas-be/app/module/articles/response" + clientsRepository "netidhub-saas-be/app/module/clients/repository" usersRepository "netidhub-saas-be/app/module/users/repository" config "netidhub-saas-be/config/config" minioStorage "netidhub-saas-be/config/config" @@ -48,6 +49,7 @@ type articlesService struct { Log zerolog.Logger Cfg *config.Config UsersRepo usersRepository.UsersRepository + ClientsRepo clientsRepository.ClientsRepository MinioStorage *minioStorage.MinioStorage // Dynamic approval system dependencies @@ -101,6 +103,7 @@ func NewArticlesService( log zerolog.Logger, cfg *config.Config, usersRepo usersRepository.UsersRepository, + clientsRepo clientsRepository.ClientsRepository, minioStorage *minioStorage.MinioStorage, ) ArticlesService { @@ -115,6 +118,7 @@ func NewArticlesService( ArticleApprovalFlowsSvc: articleApprovalFlowsSvc, Log: log, UsersRepo: usersRepo, + ClientsRepo: clientsRepo, MinioStorage: minioStorage, Cfg: cfg, } @@ -160,7 +164,7 @@ func (_i *articlesService) All(authToken string, req request.ArticlesQueryReques host := _i.Cfg.App.Domain for _, result := range results { - articleRes := mapper.ArticlesResponseMapper(_i.Log, host, clientId, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo) + articleRes := mapper.ArticlesResponseMapper(_i.Log, host, clientId, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo, _i.ClientsRepo) articless = append(articless, articleRes) } @@ -185,7 +189,7 @@ func (_i *articlesService) Show(authToken string, id uint) (articles *response.A host := _i.Cfg.App.Domain - return mapper.ArticlesResponseMapper(_i.Log, host, clientId, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo), nil + return mapper.ArticlesResponseMapper(_i.Log, host, clientId, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo, _i.ClientsRepo), nil } func (_i *articlesService) ShowByOldId(authToken string, oldId uint) (articles *response.ArticlesResponse, err error) { @@ -206,7 +210,7 @@ func (_i *articlesService) ShowByOldId(authToken string, oldId uint) (articles * host := _i.Cfg.App.Domain - return mapper.ArticlesResponseMapper(_i.Log, host, clientId, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo), nil + return mapper.ArticlesResponseMapper(_i.Log, host, clientId, result, _i.ArticleCategoriesRepo, _i.ArticleCategoryDetailsRepo, _i.ArticleFilesRepo, _i.UsersRepo, _i.ClientsRepo), nil } func (_i *articlesService) Save(authToken string, req request.ArticlesCreateRequest) (articles *entity.Articles, err error) { diff --git a/app/module/bookmarks/bookmarks.module.go b/app/module/bookmarks/bookmarks.module.go index e21a00d..0590cff 100644 --- a/app/module/bookmarks/bookmarks.module.go +++ b/app/module/bookmarks/bookmarks.module.go @@ -62,5 +62,6 @@ func (_i *BookmarksRouter) RegisterBookmarksRoutes() { router.Get("/user", bookmarksController.GetByUserId) router.Post("/toggle/:articleId", bookmarksController.ToggleBookmark) router.Get("/summary", bookmarksController.GetBookmarkSummary) + router.Get("/check/:articleId", bookmarksController.CheckBookmarkByArticleId) }) } diff --git a/app/module/bookmarks/controller/bookmarks.controller.go b/app/module/bookmarks/controller/bookmarks.controller.go index 4e57366..6e8aab4 100644 --- a/app/module/bookmarks/controller/bookmarks.controller.go +++ b/app/module/bookmarks/controller/bookmarks.controller.go @@ -26,6 +26,7 @@ type BookmarksController interface { GetByUserId(c *fiber.Ctx) error ToggleBookmark(c *fiber.Ctx) error GetBookmarkSummary(c *fiber.Ctx) error + CheckBookmarkByArticleId(c *fiber.Ctx) error } func NewBookmarksController(bookmarksService service.BookmarksService, log zerolog.Logger) BookmarksController { @@ -41,7 +42,6 @@ func NewBookmarksController(bookmarksService service.BookmarksService, log zerol // @Tags Bookmarks // @Security Bearer // @Param Authorization header string false "Insert your access token" default(Bearer ) -// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param req query request.BookmarksQueryRequest false "query parameters" // @Param req query paginator.Pagination false "pagination parameters" // @Success 200 {object} response.Response @@ -84,7 +84,6 @@ func (_i *bookmarksController) All(c *fiber.Ctx) error { // @Tags Bookmarks // @Security Bearer // @Param Authorization header string false "Insert your access token" default(Bearer ) -// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param id path int true "Bookmark ID" // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError @@ -122,7 +121,6 @@ func (_i *bookmarksController) Show(c *fiber.Ctx) error { // @Tags Bookmarks // @Security Bearer // @Param Authorization header string false "Insert your access token" default(Bearer ) -// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param req body request.BookmarksCreateRequest true "Bookmark data" // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError @@ -169,7 +167,6 @@ func (_i *bookmarksController) Save(c *fiber.Ctx) error { // @Tags Bookmarks // @Security Bearer // @Param Authorization header string false "Insert your access token" default(Bearer ) -// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param id path int true "Bookmark ID" // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError @@ -206,7 +203,6 @@ func (_i *bookmarksController) Delete(c *fiber.Ctx) error { // @Tags Bookmarks // @Security Bearer // @Param Authorization header string false "Insert your access token" default(Bearer ) -// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param req query request.BookmarksQueryRequest false "query parameters" // @Param req query paginator.Pagination false "pagination parameters" // @Success 200 {object} response.Response @@ -249,7 +245,6 @@ func (_i *bookmarksController) GetByUserId(c *fiber.Ctx) error { // @Tags Bookmarks // @Security Bearer // @Param Authorization header string false "Insert your access token" default(Bearer ) -// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Param articleId path int true "Article ID" // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError @@ -295,7 +290,6 @@ func (_i *bookmarksController) ToggleBookmark(c *fiber.Ctx) error { // @Tags Bookmarks // @Security Bearer // @Param Authorization header string false "Insert your access token" default(Bearer ) -// @Param Authorization header string false "Insert your access token" default(Bearer ) // @Success 200 {object} response.Response // @Failure 400 {object} response.BadRequestError // @Failure 401 {object} response.UnauthorizedError @@ -317,3 +311,52 @@ func (_i *bookmarksController) GetBookmarkSummary(c *fiber.Ctx) error { Data: summaryData, }) } + +// Check Bookmark by Article ID +// @Summary Check if Article is Bookmarked by Current User +// @Description API for checking if an article is bookmarked by the current user +// @Tags Bookmarks +// @Security Bearer +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param articleId path int true "Article ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /bookmarks/check/{articleId} [get] +func (_i *bookmarksController) CheckBookmarkByArticleId(c *fiber.Ctx) error { + articleId, err := strconv.Atoi(c.Params("articleId")) + if err != nil { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{"Invalid article ID"}, + }) + } + + // Get Authorization token from header + authToken := c.Get("Authorization") + _i.Log.Info().Str("authToken", authToken).Msg("") + + isBookmarked, bookmarkId, err := _i.bookmarksService.CheckBookmarkByArticleId(authToken, uint(articleId)) + if err != nil { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{err.Error()}, + }) + } + + responseData := map[string]interface{}{ + "isBookmarked": isBookmarked, + "articleId": articleId, + } + + if bookmarkId != nil { + responseData["bookmarkId"] = *bookmarkId + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Successfully checked bookmark status"}, + Data: responseData, + }) +} diff --git a/app/module/bookmarks/service/bookmarks.service.go b/app/module/bookmarks/service/bookmarks.service.go index e165c1b..ebc08a1 100644 --- a/app/module/bookmarks/service/bookmarks.service.go +++ b/app/module/bookmarks/service/bookmarks.service.go @@ -33,6 +33,7 @@ type BookmarksService interface { GetByUserId(authToken string, req request.BookmarksQueryRequest) (bookmarks []*response.BookmarksResponse, paging paginator.Pagination, err error) ToggleBookmark(authToken string, articleId uint) (isBookmarked bool, err error) GetBookmarkSummary(authToken string) (summary *response.BookmarksSummaryResponse, err error) + CheckBookmarkByArticleId(authToken string, articleId uint) (isBookmarked bool, bookmarkId *uint, err error) } // NewBookmarksService init BookmarksService @@ -124,7 +125,7 @@ func (_i *bookmarksService) Save(authToken string, req request.BookmarksCreateRe } // Check if article exists - _, err = _i.ArticlesRepo.FindOne(clientId, req.ArticleId) + _, err = _i.ArticlesRepo.FindOne(nil, req.ArticleId) if err != nil { _i.Log.Error().Err(err).Msg("Article not found") return nil, errors.New("article not found") @@ -159,7 +160,7 @@ func (_i *bookmarksService) Delete(authToken string, id uint) error { } } - err := _i.Repo.Delete(clientId, id) + err := _i.Repo.Delete(nil, id) if err != nil { _i.Log.Error().Err(err).Msg("Failed to delete bookmark") return err @@ -220,7 +221,7 @@ func (_i *bookmarksService) ToggleBookmark(authToken string, articleId uint) (is } // Check if article exists - _, err = _i.ArticlesRepo.FindOne(clientId, articleId) + _, err = _i.ArticlesRepo.FindOne(nil, articleId) if err != nil { _i.Log.Error().Err(err).Msg("Article not found") return false, errors.New("article not found") @@ -308,6 +309,45 @@ func (_i *bookmarksService) GetBookmarkSummary(authToken string) (summary *respo return summary, nil } +func (_i *bookmarksService) CheckBookmarkByArticleId(authToken string, articleId uint) (isBookmarked bool, bookmarkId *uint, err error) { + // Extract clientId from authToken + var clientId *uuid.UUID + if authToken != "" { + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user != nil && user.ClientId != nil { + clientId = user.ClientId + _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token") + } + } + + // Extract user info from auth token + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user == nil { + _i.Log.Error().Msg("User not found from auth token") + return false, nil, errors.New("user not found") + } + + // Check if article exists + _, err = _i.ArticlesRepo.FindOne(nil, articleId) + if err != nil { + _i.Log.Error().Err(err).Msg("Article not found") + return false, nil, errors.New("article not found") + } + + // Check if bookmark exists + existingBookmark, err := _i.Repo.FindByUserAndArticle(clientId, user.ID, articleId) + if err != nil { + // Bookmark doesn't exist + return false, nil, nil + } + + if existingBookmark != nil { + return true, &existingBookmark.ID, nil + } + + return false, nil, nil +} + // Helper function to create bool pointer func boolPtr(b bool) *bool { return &b diff --git a/app/module/clients/clients.module.go b/app/module/clients/clients.module.go index c6d163d..b028ad6 100644 --- a/app/module/clients/clients.module.go +++ b/app/module/clients/clients.module.go @@ -20,6 +20,9 @@ var NewClientsModule = fx.Options( // register repository of Clients module fx.Provide(repository.NewClientsRepository), + // register client logo upload service + fx.Provide(service.NewClientLogoUploadService), + // register service of Clients module fx.Provide(service.NewClientsService), @@ -51,5 +54,10 @@ func (_i *ClientsRouter) RegisterClientsRoutes() { router.Post("/with-user", clientsController.CreateClientWithUser) router.Put("/:id", clientsController.Update) router.Delete("/:id", clientsController.Delete) + + // Logo upload routes + router.Post("/:id/logo", clientsController.UploadLogo) + router.Delete("/:id/logo", clientsController.DeleteLogo) + router.Get("/:id/logo/url", clientsController.GetLogoURL) }) } diff --git a/app/module/clients/controller/clients.controller.go b/app/module/clients/controller/clients.controller.go index d25a5a7..694693a 100644 --- a/app/module/clients/controller/clients.controller.go +++ b/app/module/clients/controller/clients.controller.go @@ -35,6 +35,11 @@ type ClientsController interface { // Client with user creation CreateClientWithUser(c *fiber.Ctx) error + + // Logo upload endpoints + UploadLogo(c *fiber.Ctx) error + DeleteLogo(c *fiber.Ctx) error + GetLogoURL(c *fiber.Ctx) error } func NewClientsController(clientsService service.ClientsService, log zerolog.Logger) ClientsController { @@ -486,3 +491,148 @@ func (_i *clientsController) CreateClientWithUser(c *fiber.Ctx) error { Data: result, }) } + +// ===================================================================== +// LOGO UPLOAD ENDPOINTS +// ===================================================================== + +// UploadLogo uploads client logo +// @Summary Upload client logo +// @Description API for uploading client logo image to MinIO +// @Tags Clients +// @Security Bearer +// @Param id path string true "Client ID" +// @Param logo formData file true "Logo image file (jpg, jpeg, png, gif, webp, max 5MB)" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /clients/{id}/logo [post] +func (_i *clientsController) UploadLogo(c *fiber.Ctx) error { + clientId, err := uuid.Parse(c.Params("id")) + if err != nil { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{"Invalid client ID"}, + }) + } + + imagePath, err := _i.clientsService.UploadLogo(clientId, c) + if err != nil { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{err.Error()}, + }) + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Client logo uploaded successfully"}, + Data: map[string]string{ + "imagePath": imagePath, + }, + }) +} + +// DeleteLogo deletes client logo +// @Summary Delete client logo +// @Description API for deleting client logo from MinIO +// @Tags Clients +// @Security Bearer +// @Param id path string true "Client ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /clients/{id}/logo [delete] +func (_i *clientsController) DeleteLogo(c *fiber.Ctx) error { + clientId, err := uuid.Parse(c.Params("id")) + if err != nil { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{"Invalid client ID"}, + }) + } + + // Get current client to find image path + client, err := _i.clientsService.Show(clientId) + if err != nil { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{"Client not found"}, + }) + } + + if client.LogoImagePath == nil || *client.LogoImagePath == "" { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{"No logo found for this client"}, + }) + } + + err = _i.clientsService.DeleteLogo(clientId, *client.LogoImagePath) + if err != nil { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{err.Error()}, + }) + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Client logo deleted successfully"}, + }) +} + +// GetLogoURL generates presigned URL for client logo +// @Summary Get client logo URL +// @Description API for generating presigned URL for client logo +// @Tags Clients +// @Security Bearer +// @Param id path string true "Client ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /clients/{id}/logo/url [get] +func (_i *clientsController) GetLogoURL(c *fiber.Ctx) error { + clientId, err := uuid.Parse(c.Params("id")) + if err != nil { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{"Invalid client ID"}, + }) + } + + // Get current client to find image path + client, err := _i.clientsService.Show(clientId) + if err != nil { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{"Client not found"}, + }) + } + + if client.LogoImagePath == nil || *client.LogoImagePath == "" { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{"No logo found for this client"}, + }) + } + + url, err := _i.clientsService.GetLogoURL(*client.LogoImagePath) + if err != nil { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{err.Error()}, + }) + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Logo URL generated successfully"}, + Data: map[string]string{ + "url": url, + }, + }) +} diff --git a/app/module/clients/mapper/clients.mapper.go b/app/module/clients/mapper/clients.mapper.go index f2ef8b6..b5ae671 100644 --- a/app/module/clients/mapper/clients.mapper.go +++ b/app/module/clients/mapper/clients.mapper.go @@ -8,12 +8,23 @@ import ( func ClientsResponseMapper(clientsReq *entity.Clients) (clientsRes *res.ClientsResponse) { if clientsReq != nil { clientsRes = &res.ClientsResponse{ - ID: clientsReq.ID, - Name: clientsReq.Name, - CreatedById: clientsReq.CreatedById, - IsActive: clientsReq.IsActive, - CreatedAt: clientsReq.CreatedAt, - UpdatedAt: clientsReq.UpdatedAt, + ID: clientsReq.ID, + Name: clientsReq.Name, + Description: clientsReq.Description, + ClientType: clientsReq.ClientType, + ParentClientId: clientsReq.ParentClientId, + LogoUrl: clientsReq.LogoUrl, + LogoImagePath: clientsReq.LogoImagePath, + Address: clientsReq.Address, + PhoneNumber: clientsReq.PhoneNumber, + Website: clientsReq.Website, + MaxUsers: clientsReq.MaxUsers, + MaxStorage: clientsReq.MaxStorage, + Settings: clientsReq.Settings, + CreatedById: clientsReq.CreatedById, + IsActive: clientsReq.IsActive, + CreatedAt: clientsReq.CreatedAt, + UpdatedAt: clientsReq.UpdatedAt, } } return clientsRes diff --git a/app/module/clients/repository/clients.repository.go b/app/module/clients/repository/clients.repository.go index 2322169..5c08c33 100644 --- a/app/module/clients/repository/clients.repository.go +++ b/app/module/clients/repository/clients.repository.go @@ -23,6 +23,7 @@ type clientsRepository struct { type ClientsRepository interface { GetAll(req request.ClientsQueryRequest) (clientss []*entity.Clients, paging paginator.Pagination, err error) FindOne(id uuid.UUID) (clients *entity.Clients, err error) + FindOneByClientId(clientId *uuid.UUID) (clients *entity.Clients, err error) Create(clients *entity.Clients) (clientsReturn *entity.Clients, err error) Update(id uuid.UUID, clients *entity.Clients) (err error) Delete(id uuid.UUID) (err error) @@ -75,7 +76,7 @@ func (_i *clientsRepository) GetAll(req request.ClientsQueryRequest) (clientss [ if req.OnlyStandalone != nil && *req.OnlyStandalone { query = query.Where("client_type = ?", "standalone") } - + // Active filter if req.IsActive != nil { query = query.Where("is_active = ?", *req.IsActive) @@ -137,6 +138,18 @@ func (_i *clientsRepository) FindOne(id uuid.UUID) (clients *entity.Clients, err return clients, nil } +func (_i *clientsRepository) FindOneByClientId(clientId *uuid.UUID) (clients *entity.Clients, err error) { + if clientId == nil { + return nil, nil + } + + if err := _i.DB.DB.Where("id = ?", *clientId).First(&clients).Error; err != nil { + return nil, err + } + + return clients, nil +} + func (_i *clientsRepository) Create(clients *entity.Clients) (clientsReturn *entity.Clients, err error) { result := _i.DB.DB.Create(clients) return clients, result.Error diff --git a/app/module/clients/request/clients.request.go b/app/module/clients/request/clients.request.go index 40f0a2d..9e5f881 100644 --- a/app/module/clients/request/clients.request.go +++ b/app/module/clients/request/clients.request.go @@ -67,6 +67,13 @@ type ClientsUpdateRequest struct { Settings *string `json:"settings"` IsActive *bool `json:"isActive"` + + // Additional tenant information fields + LogoUrl *string `json:"logoUrl"` // Logo tenant URL + LogoImagePath *string `json:"logoImagePath"` // Logo image path in MinIO + Address *string `json:"address"` // Alamat + PhoneNumber *string `json:"phoneNumber"` // Nomor telepon + Website *string `json:"website"` // Website resmi } // ClientsQueryRequest for filtering and pagination diff --git a/app/module/clients/response/clients.response.go b/app/module/clients/response/clients.response.go index 53428ce..01564bd 100644 --- a/app/module/clients/response/clients.response.go +++ b/app/module/clients/response/clients.response.go @@ -18,6 +18,13 @@ type ClientsResponse struct { ClientType string `json:"clientType"` ParentClientId *uuid.UUID `json:"parentClientId"` + // Additional tenant information fields + LogoUrl *string `json:"logoUrl"` // Logo tenant URL + LogoImagePath *string `json:"logoImagePath"` // Logo image path in MinIO + Address *string `json:"address"` // Alamat + PhoneNumber *string `json:"phoneNumber"` // Nomor telepon + Website *string `json:"website"` // Website resmi + // Parent info (if sub_client) ParentClient *ParentClientInfo `json:"parentClient,omitempty"` @@ -99,16 +106,24 @@ type UserInfo struct { // ClientHierarchyResponse full tree structure type ClientHierarchyResponse struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Description *string `json:"description"` - ClientType string `json:"clientType"` - Level int `json:"level"` // Depth in tree (0 = root) - Path []string `json:"path"` // Breadcrumb path - ParentClientId *uuid.UUID `json:"parentClientId"` - SubClients []ClientHierarchyResponse `json:"subClients,omitempty"` - CurrentUsers int `json:"currentUsers"` - IsActive *bool `json:"isActive"` + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + ClientType string `json:"clientType"` + Level int `json:"level"` // Depth in tree (0 = root) + Path []string `json:"path"` // Breadcrumb path + ParentClientId *uuid.UUID `json:"parentClientId"` + + // Additional tenant information fields + LogoUrl *string `json:"logoUrl"` // Logo tenant URL + LogoImagePath *string `json:"logoImagePath"` // Logo image path in MinIO + Address *string `json:"address"` // Alamat + PhoneNumber *string `json:"phoneNumber"` // Nomor telepon + Website *string `json:"website"` // Website resmi + + SubClients []ClientHierarchyResponse `json:"subClients,omitempty"` + CurrentUsers int `json:"currentUsers"` + IsActive *bool `json:"isActive"` } // ClientStatsResponse statistics for a client diff --git a/app/module/clients/service/client_logo_upload.service.go b/app/module/clients/service/client_logo_upload.service.go new file mode 100644 index 0000000..0b1e16d --- /dev/null +++ b/app/module/clients/service/client_logo_upload.service.go @@ -0,0 +1,196 @@ +package service + +import ( + "context" + "fmt" + "math/rand" + "path/filepath" + "strings" + "time" + + "netidhub-saas-be/config/config" + + "github.com/gofiber/fiber/v2" + "github.com/minio/minio-go/v7" + "github.com/rs/zerolog" +) + +// ClientLogoUploadService handles client logo uploads to MinIO +type ClientLogoUploadService struct { + MinioStorage *config.MinioStorage + Log zerolog.Logger +} + +// NewClientLogoUploadService creates a new client logo upload service +func NewClientLogoUploadService(minioStorage *config.MinioStorage, log zerolog.Logger) *ClientLogoUploadService { + return &ClientLogoUploadService{ + MinioStorage: minioStorage, + Log: log, + } +} + +// UploadLogo uploads client logo to MinIO and returns the image path +func (s *ClientLogoUploadService) UploadLogo(c *fiber.Ctx, clientId string) (string, error) { + // Get the uploaded file + file, err := c.FormFile("logo") + if err != nil { + return "", fmt.Errorf("failed to get uploaded file: %w", err) + } + + // Validate file type + if !s.isValidImageType(file.Filename) { + return "", fmt.Errorf("invalid file type. Only jpg, jpeg, png, gif, webp are allowed") + } + + // Validate file size (max 5MB) + const maxSize = 5 * 1024 * 1024 // 5MB + if file.Size > maxSize { + return "", fmt.Errorf("file size too large. Maximum size is 5MB") + } + + // Create MinIO connection + minioClient, err := s.MinioStorage.ConnectMinio() + if err != nil { + return "", fmt.Errorf("failed to connect to MinIO: %w", err) + } + + bucketName := s.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName + + // Generate unique filename + filename := s.generateUniqueFilename(file.Filename) + objectName := fmt.Sprintf("clients/logos/%s/%s", clientId, filename) + + // Open file + src, err := file.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %w", err) + } + defer src.Close() + + // Upload to MinIO + _, err = minioClient.PutObject( + context.Background(), + bucketName, + objectName, + src, + file.Size, + minio.PutObjectOptions{ + ContentType: s.getContentType(file.Filename), + }, + ) + if err != nil { + return "", fmt.Errorf("failed to upload file to MinIO: %w", err) + } + + s.Log.Info(). + Str("clientId", clientId). + Str("filename", filename). + Str("objectName", objectName). + Int64("fileSize", file.Size). + Msg("Client logo uploaded successfully") + + return objectName, nil +} + +// DeleteLogo deletes client logo from MinIO +func (s *ClientLogoUploadService) DeleteLogo(clientId, imagePath string) error { + if imagePath == "" { + return nil // Nothing to delete + } + + // Create MinIO connection + minioClient, err := s.MinioStorage.ConnectMinio() + if err != nil { + return fmt.Errorf("failed to connect to MinIO: %w", err) + } + + bucketName := s.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName + + // Delete from MinIO + err = minioClient.RemoveObject(context.Background(), bucketName, imagePath, minio.RemoveObjectOptions{}) + if err != nil { + return fmt.Errorf("failed to delete file from MinIO: %w", err) + } + + s.Log.Info(). + Str("clientId", clientId). + Str("imagePath", imagePath). + Msg("Client logo deleted successfully") + + return nil +} + +// GetLogoURL generates a presigned URL for the logo +func (s *ClientLogoUploadService) GetLogoURL(imagePath string, expiry time.Duration) (string, error) { + if imagePath == "" { + return "", nil + } + + // Create MinIO connection + minioClient, err := s.MinioStorage.ConnectMinio() + if err != nil { + return "", fmt.Errorf("failed to connect to MinIO: %w", err) + } + + bucketName := s.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName + + // Generate presigned URL + url, err := minioClient.PresignedGetObject(context.Background(), bucketName, imagePath, expiry, nil) + if err != nil { + return "", fmt.Errorf("failed to generate presigned URL: %w", err) + } + + return url.String(), nil +} + +// isValidImageType checks if the file extension is a valid image type +func (s *ClientLogoUploadService) isValidImageType(filename string) bool { + ext := strings.ToLower(filepath.Ext(filename)) + validExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"} + + for _, validExt := range validExts { + if ext == validExt { + return true + } + } + return false +} + +// generateUniqueFilename generates a unique filename with timestamp and random number +func (s *ClientLogoUploadService) generateUniqueFilename(originalFilename string) string { + ext := filepath.Ext(originalFilename) + nameWithoutExt := strings.TrimSuffix(filepath.Base(originalFilename), ext) + + // Clean filename (remove spaces and special characters) + nameWithoutExt = strings.ReplaceAll(nameWithoutExt, " ", "_") + nameWithoutExt = strings.ReplaceAll(nameWithoutExt, "-", "_") + + // Generate unique suffix + now := time.Now() + rand.Seed(now.UnixNano()) + randomNum := rand.Intn(1000000) + + // Format: originalname_timestamp_random.ext + return fmt.Sprintf("%s_%d_%d%s", + nameWithoutExt, + now.Unix(), + randomNum, + ext) +} + +// getContentType returns the MIME type based on file extension +func (s *ClientLogoUploadService) getContentType(filename string) string { + ext := strings.ToLower(filepath.Ext(filename)) + switch ext { + case ".jpg", ".jpeg": + return "image/jpeg" + case ".png": + return "image/png" + case ".gif": + return "image/gif" + case ".webp": + return "image/webp" + default: + return "application/octet-stream" + } +} diff --git a/app/module/clients/service/clients.service.go b/app/module/clients/service/clients.service.go index 1a7a560..d5fd956 100644 --- a/app/module/clients/service/clients.service.go +++ b/app/module/clients/service/clients.service.go @@ -12,7 +12,9 @@ import ( usersRequest "netidhub-saas-be/app/module/users/request" usersService "netidhub-saas-be/app/module/users/service" "netidhub-saas-be/utils/paginator" + "time" + "github.com/gofiber/fiber/v2" "github.com/google/uuid" "github.com/rs/zerolog" @@ -21,10 +23,11 @@ import ( // ClientsService type clientsService struct { - Repo repository.ClientsRepository - UsersRepo usersRepository.UsersRepository - UsersSvc usersService.UsersService - Log zerolog.Logger + Repo repository.ClientsRepository + UsersRepo usersRepository.UsersRepository + UsersSvc usersService.UsersService + ClientLogoUploadSvc *ClientLogoUploadService + Log zerolog.Logger } // ClientsService define interface of IClientsService @@ -44,16 +47,22 @@ type ClientsService interface { // Client with user creation CreateClientWithUser(req request.ClientWithUserCreateRequest) (*response.ClientWithUserResponse, error) + + // Logo upload methods + UploadLogo(clientId uuid.UUID, c *fiber.Ctx) (string, error) + DeleteLogo(clientId uuid.UUID, imagePath string) error + GetLogoURL(imagePath string) (string, error) } // NewClientsService init ClientsService -func NewClientsService(repo repository.ClientsRepository, log zerolog.Logger, usersRepo usersRepository.UsersRepository, usersSvc usersService.UsersService) ClientsService { +func NewClientsService(repo repository.ClientsRepository, log zerolog.Logger, usersRepo usersRepository.UsersRepository, usersSvc usersService.UsersService, clientLogoUploadSvc *ClientLogoUploadService) ClientsService { return &clientsService{ - Repo: repo, - Log: log, - UsersRepo: usersRepo, - UsersSvc: usersSvc, + Repo: repo, + Log: log, + UsersRepo: usersRepo, + UsersSvc: usersSvc, + ClientLogoUploadSvc: clientLogoUploadSvc, } } @@ -125,6 +134,11 @@ func (_i *clientsService) Update(id uuid.UUID, req request.ClientsUpdateRequest) Description: req.Description, ClientType: *req.ClientType, ParentClientId: req.ParentClientId, + LogoUrl: req.LogoUrl, + LogoImagePath: req.LogoImagePath, + Address: req.Address, + PhoneNumber: req.PhoneNumber, + Website: req.Website, MaxUsers: req.MaxUsers, MaxStorage: req.MaxStorage, Settings: req.Settings, @@ -223,6 +237,11 @@ func (_i *clientsService) buildHierarchyResponse(client *entity.Clients, level i Level: level, Path: currentPath, ParentClientId: client.ParentClientId, + LogoUrl: client.LogoUrl, + LogoImagePath: client.LogoImagePath, + Address: client.Address, + PhoneNumber: client.PhoneNumber, + Website: client.Website, IsActive: client.IsActive, } @@ -410,3 +429,85 @@ func (_i *clientsService) CreateClientWithUser(req request.ClientWithUserCreateR Message: fmt.Sprintf("Client '%s' and admin user '%s' created successfully", createdClient.Name, createdUser.Username), }, nil } + +// ===================================================================== +// LOGO UPLOAD METHODS +// ===================================================================== + +// UploadLogo uploads client logo to MinIO +func (_i *clientsService) UploadLogo(clientId uuid.UUID, c *fiber.Ctx) (string, error) { + _i.Log.Info().Str("clientId", clientId.String()).Msg("Uploading client logo") + + // Upload logo using the upload service + imagePath, err := _i.ClientLogoUploadSvc.UploadLogo(c, clientId.String()) + if err != nil { + _i.Log.Error().Err(err).Str("clientId", clientId.String()).Msg("Failed to upload client logo") + return "", err + } + + // Update client with new logo image path + updateReq := request.ClientsUpdateRequest{ + LogoImagePath: &imagePath, + } + + err = _i.Update(clientId, updateReq) + if err != nil { + _i.Log.Error().Err(err).Str("clientId", clientId.String()).Str("imagePath", imagePath).Msg("Failed to update client with logo path") + + // Try to clean up uploaded file + cleanupErr := _i.ClientLogoUploadSvc.DeleteLogo(clientId.String(), imagePath) + if cleanupErr != nil { + _i.Log.Error().Err(cleanupErr).Str("imagePath", imagePath).Msg("Failed to cleanup uploaded logo after update failure") + } + + return "", fmt.Errorf("failed to update client with logo path: %w", err) + } + + _i.Log.Info().Str("clientId", clientId.String()).Str("imagePath", imagePath).Msg("Client logo uploaded and updated successfully") + return imagePath, nil +} + +// DeleteLogo deletes client logo from MinIO +func (_i *clientsService) DeleteLogo(clientId uuid.UUID, imagePath string) error { + _i.Log.Info().Str("clientId", clientId.String()).Str("imagePath", imagePath).Msg("Deleting client logo") + + err := _i.ClientLogoUploadSvc.DeleteLogo(clientId.String(), imagePath) + if err != nil { + _i.Log.Error().Err(err).Str("clientId", clientId.String()).Str("imagePath", imagePath).Msg("Failed to delete client logo") + return err + } + + // Clear logo image path from client + emptyPath := "" + updateReq := request.ClientsUpdateRequest{ + LogoImagePath: &emptyPath, + } + + err = _i.Update(clientId, updateReq) + if err != nil { + _i.Log.Error().Err(err).Str("clientId", clientId.String()).Msg("Failed to clear logo path from client") + return fmt.Errorf("failed to clear logo path from client: %w", err) + } + + _i.Log.Info().Str("clientId", clientId.String()).Msg("Client logo deleted successfully") + return nil +} + +// GetLogoURL generates a presigned URL for the logo +func (_i *clientsService) GetLogoURL(imagePath string) (string, error) { + if imagePath == "" { + return "", nil + } + + _i.Log.Info().Str("imagePath", imagePath).Msg("Generating logo URL") + + // Generate presigned URL valid for 24 hours + url, err := _i.ClientLogoUploadSvc.GetLogoURL(imagePath, 24*time.Hour) + if err != nil { + _i.Log.Error().Err(err).Str("imagePath", imagePath).Msg("Failed to generate logo URL") + return "", err + } + + _i.Log.Info().Str("imagePath", imagePath).Str("url", url).Msg("Logo URL generated successfully") + return url, nil +} diff --git a/docs/ARTICLES_CLIENT_NAME_FIELD.md b/docs/ARTICLES_CLIENT_NAME_FIELD.md new file mode 100644 index 0000000..03c034d --- /dev/null +++ b/docs/ARTICLES_CLIENT_NAME_FIELD.md @@ -0,0 +1,104 @@ +# Articles Response - Client Name Field + +## Overview + +Field `clientName` telah ditambahkan ke `ArticlesResponse` untuk memberikan informasi nama client yang memiliki artikel tersebut. + +## Field Baru + +### ArticlesResponse +```go +type ArticlesResponse struct { + // ... existing fields ... + CreatedByName *string `json:"createdByName"` + ClientName *string `json:"clientName"` // NEW FIELD + // ... other fields ... +} +``` + +## Implementasi + +### 1. Response Structure +- Field `ClientName` ditambahkan ke `ArticlesResponse` struct +- Type: `*string` (optional field) +- JSON tag: `"clientName"` + +### 2. Mapper Updates +- `ArticlesResponseMapper` diperbarui untuk melakukan lookup nama client +- Menggunakan `clientsRepository.FindOneByClientId()` untuk mendapatkan nama client +- Field `ClientName` diisi berdasarkan `articlesReq.ClientId` + +### 3. Service Updates +- `ArticlesService` diperbarui untuk menyertakan `clientsRepository` dependency +- Constructor `NewArticlesService` menambahkan parameter `clientsRepository` +- Semua pemanggilan `ArticlesResponseMapper` diperbarui untuk menyertakan parameter baru + +### 4. Repository Updates +- `ClientsRepository` interface ditambahkan method `FindOneByClientId(clientId *uuid.UUID)` +- Implementasi method untuk mencari client berdasarkan ID + +## API Response Example + +### Before +```json +{ + "success": true, + "data": { + "id": 1, + "title": "Sample Article", + "createdByName": "John Doe", + "clientId": "123e4567-e89b-12d3-a456-426614174000" + } +} +``` + +### After +```json +{ + "success": true, + "data": { + "id": 1, + "title": "Sample Article", + "createdByName": "John Doe", + "clientName": "Acme Corporation", + "clientId": "123e4567-e89b-12d3-a456-426614174000" + } +} +``` + +## Database Relationship + +``` +articles.client_id -> clients.id -> clients.name +``` + +- `articles.client_id` (UUID) merujuk ke `clients.id` +- `clients.name` (string) adalah nama client yang akan ditampilkan + +## Error Handling + +- Jika `articlesReq.ClientId` adalah `nil`, maka `ClientName` akan menjadi `""` +- Jika client tidak ditemukan, maka `ClientName` akan menjadi `""` +- Tidak ada error yang di-throw untuk missing client (graceful handling) + +## Performance Considerations + +- Lookup client dilakukan per artikel (N+1 query pattern) +- Untuk performa yang lebih baik di masa depan, pertimbangkan: + - Batch lookup untuk multiple articles + - Preloading client data dalam repository query + - Caching client names + +## Usage + +Field `clientName` akan otomatis tersedia di semua endpoint articles: +- `GET /articles` - List articles +- `GET /articles/{id}` - Get single article +- `GET /articles/old-id/{id}` - Get article by old ID + +## Migration + +Tidak ada perubahan database yang diperlukan karena: +- Field `client_id` sudah ada di tabel `articles` +- Field `name` sudah ada di tabel `clients` +- Hanya menambahkan field response untuk menampilkan nama client diff --git a/docs/BOOKMARK_CHECK_API.md b/docs/BOOKMARK_CHECK_API.md new file mode 100644 index 0000000..dd575a8 --- /dev/null +++ b/docs/BOOKMARK_CHECK_API.md @@ -0,0 +1,110 @@ +# Bookmark Check API Documentation + +## Overview +API endpoint untuk mengecek apakah suatu artikel sudah di-bookmark oleh user yang sedang login. + +## Endpoint + +### Check Bookmark by Article ID +**GET** `/bookmarks/check/{articleId}` + +#### Description +Mengecek apakah artikel dengan ID tertentu sudah di-bookmark oleh user yang sedang login. + +#### Parameters +- **articleId** (path, required): ID artikel yang akan dicek status bookmarknya + +#### Headers +- **Authorization** (required): Bearer token untuk autentikasi user + +#### Response + +##### Success Response (200) +```json +{ + "success": true, + "messages": ["Successfully checked bookmark status"], + "data": { + "isBookmarked": true, + "articleId": 123, + "bookmarkId": 456 + } +} +``` + +##### Response Fields +- **isBookmarked** (boolean): Status apakah artikel sudah di-bookmark atau belum +- **articleId** (integer): ID artikel yang dicek +- **bookmarkId** (integer, optional): ID bookmark jika artikel sudah di-bookmark + +#### Error Responses + +##### 400 Bad Request +```json +{ + "success": false, + "messages": ["Invalid article ID"] +} +``` + +##### 401 Unauthorized +```json +{ + "success": false, + "messages": ["user not found"] +} +``` + +##### 500 Internal Server Error +```json +{ + "success": false, + "messages": ["article not found"] +} +``` + +## Usage Examples + +### cURL Example +```bash +curl -X GET "http://localhost:8080/bookmarks/check/123" \ + -H "Authorization: Bearer YOUR_TOKEN_HERE" +``` + +### JavaScript Example +```javascript +const checkBookmark = async (articleId) => { + try { + const response = await fetch(`/bookmarks/check/${articleId}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + + if (data.success) { + console.log('Is bookmarked:', data.data.isBookmarked); + if (data.data.bookmarkId) { + console.log('Bookmark ID:', data.data.bookmarkId); + } + } + } catch (error) { + console.error('Error checking bookmark:', error); + } +}; +``` + +## Use Cases +1. **Frontend Bookmark Status**: Mengecek status bookmark untuk menampilkan icon bookmark yang sesuai (filled/unfilled) +2. **Conditional UI**: Menampilkan tombol "Remove Bookmark" atau "Add Bookmark" berdasarkan status +3. **Bookmark Counter**: Menghitung jumlah bookmark yang dimiliki user +4. **Bookmark Management**: Validasi sebelum melakukan operasi bookmark lainnya + +## Notes +- Endpoint ini menggunakan middleware `UserMiddleware` untuk mengekstrak informasi user dari JWT token +- Jika artikel tidak ditemukan, akan mengembalikan error 500 +- Jika user tidak ditemukan dari token, akan mengembalikan error 401 +- Field `bookmarkId` hanya akan ada jika `isBookmarked` bernilai `true` diff --git a/docs/CLIENT_LOGO_UPLOAD_API.md b/docs/CLIENT_LOGO_UPLOAD_API.md new file mode 100644 index 0000000..7aa132b --- /dev/null +++ b/docs/CLIENT_LOGO_UPLOAD_API.md @@ -0,0 +1,238 @@ +# Client Logo Upload API Documentation + +## Overview + +API untuk mengelola logo client dengan integrasi MinIO storage. Mendukung upload, delete, dan generate presigned URL untuk logo client. + +## Endpoints + +### 1. Upload Client Logo + +**POST** `/clients/{id}/logo` + +Upload logo image untuk client tertentu. + +#### Request +- **Content-Type**: `multipart/form-data` +- **Parameter**: + - `id` (path): Client ID (UUID) + - `logo` (form-data): File image (jpg, jpeg, png, gif, webp, max 5MB) + +#### Response +```json +{ + "success": true, + "messages": ["Client logo uploaded successfully"], + "data": { + "imagePath": "clients/logos/{clientId}/filename_timestamp_random.ext" + } +} +``` + +#### Example cURL +```bash +curl -X POST \ + http://localhost:8080/clients/123e4567-e89b-12d3-a456-426614174000/logo \ + -H "Authorization: Bearer your-token" \ + -F "logo=@/path/to/logo.png" +``` + +### 2. Delete Client Logo + +**DELETE** `/clients/{id}/logo` + +Hapus logo client dari MinIO dan database. + +#### Request +- **Parameter**: + - `id` (path): Client ID (UUID) + +#### Response +```json +{ + "success": true, + "messages": ["Client logo deleted successfully"] +} +``` + +#### Example cURL +```bash +curl -X DELETE \ + http://localhost:8080/clients/123e4567-e89b-12d3-a456-426614174000/logo \ + -H "Authorization: Bearer your-token" +``` + +### 3. Get Logo URL + +**GET** `/clients/{id}/logo/url` + +Generate presigned URL untuk mengakses logo client (valid 24 jam). + +#### Request +- **Parameter**: + - `id` (path): Client ID (UUID) + +#### Response +```json +{ + "success": true, + "messages": ["Logo URL generated successfully"], + "data": { + "url": "https://minio.example.com/bucket/clients/logos/{clientId}/filename?X-Amz-Algorithm=..." + } +} +``` + +#### Example cURL +```bash +curl -X GET \ + http://localhost:8080/clients/123e4567-e89b-12d3-a456-426614174000/logo/url \ + -H "Authorization: Bearer your-token" +``` + +## File Validation + +### Supported Formats +- JPEG (.jpg, .jpeg) +- PNG (.png) +- GIF (.gif) +- WebP (.webp) + +### File Size Limit +- Maximum: 5MB + +### File Naming +Uploaded files akan di-rename dengan format: +``` +{original_name}_{timestamp}_{random_number}.{extension} +``` + +Example: `company_logo_1640995200_123456.png` + +## Storage Structure + +Logo disimpan di MinIO dengan struktur path: +``` +clients/logos/{clientId}/{filename} +``` + +## Error Handling + +### Common Errors + +#### 400 Bad Request +```json +{ + "success": false, + "messages": ["Invalid file type. Only jpg, jpeg, png, gif, webp are allowed"] +} +``` + +#### 413 Payload Too Large +```json +{ + "success": false, + "messages": ["File size too large. Maximum size is 5MB"] +} +``` + +#### 404 Not Found +```json +{ + "success": false, + "messages": ["Client not found"] +} +``` + +#### 500 Internal Server Error +```json +{ + "success": false, + "messages": ["Failed to upload file to MinIO"] +} +``` + +## Database Schema + +### New Fields Added to `clients` Table + +```sql +ALTER TABLE clients +ADD COLUMN logo_url VARCHAR(255), -- External logo URL +ADD COLUMN logo_image_path VARCHAR(255), -- MinIO storage path +ADD COLUMN address TEXT, -- Physical address +ADD COLUMN phone_number VARCHAR(50), -- Contact phone +ADD COLUMN website VARCHAR(255); -- Official website +``` + +## Usage Examples + +### Frontend Integration + +#### Upload Logo +```javascript +const uploadLogo = async (clientId, file) => { + const formData = new FormData(); + formData.append('logo', file); + + const response = await fetch(`/clients/${clientId}/logo`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}` + }, + body: formData + }); + + return response.json(); +}; +``` + +#### Get Logo URL +```javascript +const getLogoUrl = async (clientId) => { + const response = await fetch(`/clients/${clientId}/logo/url`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + const result = await response.json(); + return result.data.url; +}; +``` + +### Backend Integration + +#### Update Client with Logo Path +```go +updateReq := request.ClientsUpdateRequest{ + LogoImagePath: &imagePath, + // other fields... +} + +err := clientsService.Update(clientId, updateReq) +``` + +## Security Considerations + +1. **Authentication**: Semua endpoint memerlukan Bearer token +2. **File Validation**: Validasi format dan ukuran file +3. **Presigned URLs**: URL hanya valid 24 jam +4. **Cleanup**: File lama otomatis dihapus saat upload baru + +## Performance Notes + +1. **File Size**: Batasi ukuran file untuk performa optimal +2. **CDN**: Pertimbangkan menggunakan CDN untuk distribusi logo +3. **Caching**: Cache presigned URLs di frontend +4. **Indexing**: Database indexes untuk query yang efisien + +## Migration + +Jalankan migration untuk menambahkan kolom baru: + +```sql +-- File: docs/migrations/002_add_client_tenant_fields.sql +\i docs/migrations/002_add_client_tenant_fields.sql +``` diff --git a/docs/migrations/002_add_client_tenant_fields.sql b/docs/migrations/002_add_client_tenant_fields.sql new file mode 100644 index 0000000..3701749 --- /dev/null +++ b/docs/migrations/002_add_client_tenant_fields.sql @@ -0,0 +1,24 @@ +-- Migration: Add tenant information fields to clients table +-- Date: 2025-01-27 +-- Description: Add logo_url, address, phone_number, website fields to clients table + +-- Add new columns to clients table +ALTER TABLE clients +ADD COLUMN logo_url VARCHAR(255), +ADD COLUMN logo_image_path VARCHAR(255), +ADD COLUMN address TEXT, +ADD COLUMN phone_number VARCHAR(50), +ADD COLUMN website VARCHAR(255); + +-- Add comments for documentation +COMMENT ON COLUMN clients.logo_url IS 'URL of tenant logo'; +COMMENT ON COLUMN clients.logo_image_path IS 'Path to logo image in MinIO storage'; +COMMENT ON COLUMN clients.address IS 'Physical address of the tenant'; +COMMENT ON COLUMN clients.phone_number IS 'Contact phone number of the tenant'; +COMMENT ON COLUMN clients.website IS 'Official website URL of the tenant'; + +-- Optional: Create indexes for better query performance +CREATE INDEX IF NOT EXISTS idx_clients_logo_url ON clients(logo_url); +CREATE INDEX IF NOT EXISTS idx_clients_logo_image_path ON clients(logo_image_path); +CREATE INDEX IF NOT EXISTS idx_clients_phone_number ON clients(phone_number); +CREATE INDEX IF NOT EXISTS idx_clients_website ON clients(website); diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 1eb3652..2e6b1f7 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -8092,13 +8092,6 @@ const docTemplate = `{ ], "summary": "Get all Bookmarks", "parameters": [ - { - "type": "string", - "default": "Bearer \u003cAdd access token here\u003e", - "description": "Insert your access token", - "name": "Authorization", - "in": "header" - }, { "type": "string", "default": "Bearer \u003cAdd access token here\u003e", @@ -8196,13 +8189,6 @@ const docTemplate = `{ ], "summary": "Create new Bookmark", "parameters": [ - { - "type": "string", - "default": "Bearer \u003cAdd access token here\u003e", - "description": "Insert your access token", - "name": "Authorization", - "in": "header" - }, { "type": "string", "default": "Bearer \u003cAdd access token here\u003e", @@ -8248,6 +8234,62 @@ const docTemplate = `{ } } }, + "/bookmarks/check/{articleId}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for checking if an article is bookmarked by the current user", + "tags": [ + "Bookmarks" + ], + "summary": "Check if Article is Bookmarked by Current User", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Article ID", + "name": "articleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/bookmarks/summary": { "get": { "security": [ @@ -8261,13 +8303,6 @@ const docTemplate = `{ ], "summary": "Get Bookmark Summary for User", "parameters": [ - { - "type": "string", - "default": "Bearer \u003cAdd access token here\u003e", - "description": "Insert your access token", - "name": "Authorization", - "in": "header" - }, { "type": "string", "default": "Bearer \u003cAdd access token here\u003e", @@ -8317,13 +8352,6 @@ const docTemplate = `{ ], "summary": "Toggle Bookmark (Add/Remove)", "parameters": [ - { - "type": "string", - "default": "Bearer \u003cAdd access token here\u003e", - "description": "Insert your access token", - "name": "Authorization", - "in": "header" - }, { "type": "string", "default": "Bearer \u003cAdd access token here\u003e", @@ -8380,13 +8408,6 @@ const docTemplate = `{ ], "summary": "Get Bookmarks by User ID", "parameters": [ - { - "type": "string", - "default": "Bearer \u003cAdd access token here\u003e", - "description": "Insert your access token", - "name": "Authorization", - "in": "header" - }, { "type": "string", "default": "Bearer \u003cAdd access token here\u003e", @@ -8486,13 +8507,6 @@ const docTemplate = `{ ], "summary": "Get Bookmark by ID", "parameters": [ - { - "type": "string", - "default": "Bearer \u003cAdd access token here\u003e", - "description": "Insert your access token", - "name": "Authorization", - "in": "header" - }, { "type": "string", "default": "Bearer \u003cAdd access token here\u003e", @@ -8547,13 +8561,6 @@ const docTemplate = `{ ], "summary": "Delete Bookmark", "parameters": [ - { - "type": "string", - "default": "Bearer \u003cAdd access token here\u003e", - "description": "Insert your access token", - "name": "Authorization", - "in": "header" - }, { "type": "string", "default": "Bearer \u003cAdd access token here\u003e", @@ -9978,6 +9985,158 @@ const docTemplate = `{ } } }, + "/clients/{id}/logo": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for uploading client logo image to MinIO", + "tags": [ + "Clients" + ], + "summary": "Upload client logo", + "parameters": [ + { + "type": "string", + "description": "Client ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "file", + "description": "Logo image file (jpg, jpeg, png, gif, webp, max 5MB)", + "name": "logo", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for deleting client logo from MinIO", + "tags": [ + "Clients" + ], + "summary": "Delete client logo", + "parameters": [ + { + "type": "string", + "description": "Client ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, + "/clients/{id}/logo/url": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for generating presigned URL for client logo", + "tags": [ + "Clients" + ], + "summary": "Get client logo URL", + "parameters": [ + { + "type": "string", + "description": "Client ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/clients/{id}/move": { "put": { "security": [ @@ -17416,6 +17575,10 @@ const docTemplate = `{ "request.ClientsUpdateRequest": { "type": "object", "properties": { + "address": { + "description": "Alamat", + "type": "string" + }, "clientType": { "type": "string", "enum": [ @@ -17430,6 +17593,14 @@ const docTemplate = `{ "isActive": { "type": "boolean" }, + "logoImagePath": { + "description": "Logo image path in MinIO", + "type": "string" + }, + "logoUrl": { + "description": "Additional tenant information fields", + "type": "string" + }, "maxStorage": { "type": "integer" }, @@ -17443,9 +17614,17 @@ const docTemplate = `{ "parentClientId": { "type": "string" }, + "phoneNumber": { + "description": "Nomor telepon", + "type": "string" + }, "settings": { "description": "Custom settings", "type": "string" + }, + "website": { + "description": "Website resmi", + "type": "string" } } }, diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 96737f5..5640b50 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -8081,13 +8081,6 @@ ], "summary": "Get all Bookmarks", "parameters": [ - { - "type": "string", - "default": "Bearer \u003cAdd access token here\u003e", - "description": "Insert your access token", - "name": "Authorization", - "in": "header" - }, { "type": "string", "default": "Bearer \u003cAdd access token here\u003e", @@ -8185,13 +8178,6 @@ ], "summary": "Create new Bookmark", "parameters": [ - { - "type": "string", - "default": "Bearer \u003cAdd access token here\u003e", - "description": "Insert your access token", - "name": "Authorization", - "in": "header" - }, { "type": "string", "default": "Bearer \u003cAdd access token here\u003e", @@ -8237,6 +8223,62 @@ } } }, + "/bookmarks/check/{articleId}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for checking if an article is bookmarked by the current user", + "tags": [ + "Bookmarks" + ], + "summary": "Check if Article is Bookmarked by Current User", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "Article ID", + "name": "articleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/bookmarks/summary": { "get": { "security": [ @@ -8250,13 +8292,6 @@ ], "summary": "Get Bookmark Summary for User", "parameters": [ - { - "type": "string", - "default": "Bearer \u003cAdd access token here\u003e", - "description": "Insert your access token", - "name": "Authorization", - "in": "header" - }, { "type": "string", "default": "Bearer \u003cAdd access token here\u003e", @@ -8306,13 +8341,6 @@ ], "summary": "Toggle Bookmark (Add/Remove)", "parameters": [ - { - "type": "string", - "default": "Bearer \u003cAdd access token here\u003e", - "description": "Insert your access token", - "name": "Authorization", - "in": "header" - }, { "type": "string", "default": "Bearer \u003cAdd access token here\u003e", @@ -8369,13 +8397,6 @@ ], "summary": "Get Bookmarks by User ID", "parameters": [ - { - "type": "string", - "default": "Bearer \u003cAdd access token here\u003e", - "description": "Insert your access token", - "name": "Authorization", - "in": "header" - }, { "type": "string", "default": "Bearer \u003cAdd access token here\u003e", @@ -8475,13 +8496,6 @@ ], "summary": "Get Bookmark by ID", "parameters": [ - { - "type": "string", - "default": "Bearer \u003cAdd access token here\u003e", - "description": "Insert your access token", - "name": "Authorization", - "in": "header" - }, { "type": "string", "default": "Bearer \u003cAdd access token here\u003e", @@ -8536,13 +8550,6 @@ ], "summary": "Delete Bookmark", "parameters": [ - { - "type": "string", - "default": "Bearer \u003cAdd access token here\u003e", - "description": "Insert your access token", - "name": "Authorization", - "in": "header" - }, { "type": "string", "default": "Bearer \u003cAdd access token here\u003e", @@ -9967,6 +9974,158 @@ } } }, + "/clients/{id}/logo": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for uploading client logo image to MinIO", + "tags": [ + "Clients" + ], + "summary": "Upload client logo", + "parameters": [ + { + "type": "string", + "description": "Client ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "file", + "description": "Logo image file (jpg, jpeg, png, gif, webp, max 5MB)", + "name": "logo", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for deleting client logo from MinIO", + "tags": [ + "Clients" + ], + "summary": "Delete client logo", + "parameters": [ + { + "type": "string", + "description": "Client ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, + "/clients/{id}/logo/url": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for generating presigned URL for client logo", + "tags": [ + "Clients" + ], + "summary": "Get client logo URL", + "parameters": [ + { + "type": "string", + "description": "Client ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/response.UnauthorizedError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/clients/{id}/move": { "put": { "security": [ @@ -17405,6 +17564,10 @@ "request.ClientsUpdateRequest": { "type": "object", "properties": { + "address": { + "description": "Alamat", + "type": "string" + }, "clientType": { "type": "string", "enum": [ @@ -17419,6 +17582,14 @@ "isActive": { "type": "boolean" }, + "logoImagePath": { + "description": "Logo image path in MinIO", + "type": "string" + }, + "logoUrl": { + "description": "Additional tenant information fields", + "type": "string" + }, "maxStorage": { "type": "integer" }, @@ -17432,9 +17603,17 @@ "parentClientId": { "type": "string" }, + "phoneNumber": { + "description": "Nomor telepon", + "type": "string" + }, "settings": { "description": "Custom settings", "type": "string" + }, + "website": { + "description": "Website resmi", + "type": "string" } } }, diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 3de4fdc..fa2e121 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -779,6 +779,9 @@ definitions: type: object request.ClientsUpdateRequest: properties: + address: + description: Alamat + type: string clientType: enum: - parent_client @@ -789,6 +792,12 @@ definitions: type: string isActive: type: boolean + logoImagePath: + description: Logo image path in MinIO + type: string + logoUrl: + description: Additional tenant information fields + type: string maxStorage: type: integer maxUsers: @@ -798,9 +807,15 @@ definitions: type: string parentClientId: type: string + phoneNumber: + description: Nomor telepon + type: string settings: description: Custom settings type: string + website: + description: Website resmi + type: string type: object request.CreateApprovalWorkflowStepsRequest: properties: @@ -6839,11 +6854,6 @@ paths: get: description: API for getting all Bookmarks parameters: - - default: Bearer - description: Insert your access token - in: header - name: Authorization - type: string - default: Bearer description: Insert your access token in: header @@ -6904,11 +6914,6 @@ paths: post: description: API for creating new Bookmark parameters: - - default: Bearer - description: Insert your access token - in: header - name: Authorization - type: string - default: Bearer description: Insert your access token in: header @@ -6946,11 +6951,6 @@ paths: delete: description: API for deleting Bookmark parameters: - - default: Bearer - description: Insert your access token - in: header - name: Authorization - type: string - default: Bearer description: Insert your access token in: header @@ -6986,11 +6986,6 @@ paths: get: description: API for getting Bookmark by ID parameters: - - default: Bearer - description: Insert your access token - in: header - name: Authorization - type: string - default: Bearer description: Insert your access token in: header @@ -7023,16 +7018,47 @@ paths: summary: Get Bookmark by ID tags: - Bookmarks - /bookmarks/summary: + /bookmarks/check/{articleId}: get: - description: API for getting bookmark summary including total count and recent - bookmarks + description: API for checking if an article is bookmarked by the current user parameters: - default: Bearer description: Insert your access token in: header name: Authorization type: string + - description: Article ID + in: path + name: articleId + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Check if Article is Bookmarked by Current User + tags: + - Bookmarks + /bookmarks/summary: + get: + description: API for getting bookmark summary including total count and recent + bookmarks + parameters: - default: Bearer description: Insert your access token in: header @@ -7064,11 +7090,6 @@ paths: post: description: API for toggling bookmark status for an article parameters: - - default: Bearer - description: Insert your access token - in: header - name: Authorization - type: string - default: Bearer description: Insert your access token in: header @@ -7105,11 +7126,6 @@ paths: get: description: API for getting Bookmarks by User ID parameters: - - default: Bearer - description: Insert your access token - in: header - name: Authorization - type: string - default: Bearer description: Insert your access token in: header @@ -7995,6 +8011,103 @@ paths: summary: Get client hierarchy tags: - Clients + /clients/{id}/logo: + delete: + description: API for deleting client logo from MinIO + parameters: + - description: Client ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Delete client logo + tags: + - Clients + post: + description: API for uploading client logo image to MinIO + parameters: + - description: Client ID + in: path + name: id + required: true + type: string + - description: Logo image file (jpg, jpeg, png, gif, webp, max 5MB) + in: formData + name: logo + required: true + type: file + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Upload client logo + tags: + - Clients + /clients/{id}/logo/url: + get: + description: API for generating presigned URL for client logo + parameters: + - description: Client ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/response.UnauthorizedError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + security: + - Bearer: [] + summary: Get client logo URL + tags: + - Clients /clients/{id}/move: put: description: API for moving a client to different parent