fix: update articles response, add client updates, update bookmarks
This commit is contained in:
parent
118132be33
commit
d6eb8fece3
|
|
@ -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()"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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`
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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);
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue