fix: update articles response, add client updates, update bookmarks

This commit is contained in:
hanif salafi 2025-10-12 12:02:55 +07:00
parent 118132be33
commit d6eb8fece3
22 changed files with 1778 additions and 217 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <Add access token here>)
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @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 <Add access token here>)
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @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 <Add access token here>)
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @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 <Add access token here>)
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @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 <Add access token here>)
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @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 <Add access token here>)
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @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 <Add access token here>)
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @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 <Add access token here>)
// @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,
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

110
docs/BOOKMARK_CHECK_API.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
@ -6904,11 +6914,6 @@ paths:
post:
description: API for creating new Bookmark
parameters:
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
@ -6946,11 +6951,6 @@ paths:
delete:
description: API for deleting Bookmark
parameters:
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
@ -6986,11 +6986,6 @@ paths:
get:
description: API for getting Bookmark by ID
parameters:
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- default: Bearer <Add access token here>
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 <Add access token here>
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 <Add access token here>
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 <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- default: Bearer <Add access token here>
description: Insert your access token
in: header
@ -7105,11 +7126,6 @@ paths:
get:
description: API for getting Bookmarks by User ID
parameters:
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
type: string
- default: Bearer <Add access token here>
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