From 52b64324303ca0aa400151a3a239527570d2ec97 Mon Sep 17 00:00:00 2001 From: Rama Priyanto Date: Thu, 18 Dec 2025 07:38:32 +0700 Subject: [PATCH] fix:toml, yaml, temp upload file --- .gitlab-ci.yml | 8 +- app/database/entity/ai_chat_files.go | 22 + app/database/index.database.go | 1 + .../ai_chat_files/ai_chat_files.module.go | 59 +++ .../controller/article_files.controller.go | 239 +++++++++ .../ai_chat_files/controller/controller.go | 13 + .../mapper/article_files.mapper.go | 29 ++ .../repository/article_files.repository.go | 123 +++++ .../request/ai_chat_files.request.go | 112 ++++ .../response/ai_chat_files.response.go | 17 + .../service/ai_chat_files.service.go | 446 ++++++++++++++++ .../service/async_uploader.service.go | 139 +++++ .../service/upload_manager.service.go | 71 +++ app/router/api.go | 7 + config/toml/config.toml | 8 +- docs/swagger/docs.go | 485 ++++++++++++++++++ docs/swagger/swagger.json | 485 ++++++++++++++++++ docs/swagger/swagger.yaml | 310 +++++++++++ main.go | 2 + 19 files changed, 2568 insertions(+), 8 deletions(-) create mode 100644 app/database/entity/ai_chat_files.go create mode 100644 app/module/ai_chat_files/ai_chat_files.module.go create mode 100644 app/module/ai_chat_files/controller/article_files.controller.go create mode 100644 app/module/ai_chat_files/controller/controller.go create mode 100644 app/module/ai_chat_files/mapper/article_files.mapper.go create mode 100644 app/module/ai_chat_files/repository/article_files.repository.go create mode 100644 app/module/ai_chat_files/request/ai_chat_files.request.go create mode 100644 app/module/ai_chat_files/response/ai_chat_files.response.go create mode 100644 app/module/ai_chat_files/service/ai_chat_files.service.go create mode 100644 app/module/ai_chat_files/service/async_uploader.service.go create mode 100644 app/module/ai_chat_files/service/upload_manager.service.go diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 43a9166..00700c8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,10 +13,10 @@ stages: build-2: stage: build-image - image: docker/compose:latest + image: docker/compose:25.0.3-cli services: - - name: docker:dind - command: [ "--insecure-registry=103.82.242.92:8900" ] + - name: docker:25.0.3-dind + command: ["--insecure-registry=103.82.242.92:8900"] script: - docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 103.82.242.92:8900 - docker-compose build @@ -30,4 +30,4 @@ deploy: services: - docker:dind script: - - curl --user $JENKINS_USER:$JENKINS_PWD http://38.47.180.165:8080/job/autodeploy-narasiahli-be/build?token=autodeploynarasiahli \ No newline at end of file + - curl --user $JENKINS_USER:$JENKINS_PWD http://103.31.38.120:8080/job/autodeploy-narasiahli-be/build?token=autodeploynarasiahli diff --git a/app/database/entity/ai_chat_files.go b/app/database/entity/ai_chat_files.go new file mode 100644 index 0000000..d7a35d9 --- /dev/null +++ b/app/database/entity/ai_chat_files.go @@ -0,0 +1,22 @@ +package entity + +import ( + "time" +) + +type AiChatFiles struct { + ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` + MessageId uint `json:"message_id" gorm:"type:int4"` + UploadID *string `json:"upload_id" gorm:"type:varchar"` + Type string `json:"type" gorm:"type:varchar"` + FilePath *string `json:"file_path" gorm:"type:varchar"` + FileUrl *string `json:"file_url" gorm:"type:varchar"` + FileName *string `json:"file_name" gorm:"type:varchar"` + FileThumbnail *string `json:"file_thumbnail" gorm:"type:varchar"` + FileAlt *string `json:"file_alt" gorm:"type:varchar"` + Size *string `json:"size" gorm:"type:varchar"` + FileType *string `json:"file_type" gorm:"type:varchar"` + IsActive bool `json:"is_active" gorm:"type:bool;default:true"` + CreatedAt time.Time `json:"created_at" gorm:"default:now()"` + UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` +} diff --git a/app/database/index.database.go b/app/database/index.database.go index fa98d1c..8003da6 100644 --- a/app/database/index.database.go +++ b/app/database/index.database.go @@ -129,6 +129,7 @@ func Models() []interface{} { entity.AIChatSessions{}, entity.AIChatMessages{}, entity.AIChatLogs{}, + entity.AiChatFiles{}, // Ebook entities entity.Ebooks{}, diff --git a/app/module/ai_chat_files/ai_chat_files.module.go b/app/module/ai_chat_files/ai_chat_files.module.go new file mode 100644 index 0000000..2d447aa --- /dev/null +++ b/app/module/ai_chat_files/ai_chat_files.module.go @@ -0,0 +1,59 @@ +package ai_chat_files + +import ( + "narasi-ahli-be/app/module/ai_chat_files/controller" + "narasi-ahli-be/app/module/ai_chat_files/repository" + "narasi-ahli-be/app/module/ai_chat_files/service" + + "github.com/gofiber/fiber/v2" + "go.uber.org/fx" +) + +// struct of ArticleFilesRouter +type AiChatFilesRouter struct { + App fiber.Router + Controller *controller.Controller +} + +// register bulky of AiChatFiles module +var NewAiChatFilesModule = fx.Options( + // register repository of AiChatFiles module + fx.Provide(repository.NewAiChatFilesRepository), + + // register service of AiChatFiles module + fx.Provide(service.NewAiChatFilesService), + fx.Provide(service.NewUploadService), + fx.Provide(service.NewUploadManager), + + // register controller of AiChatFiles module + fx.Provide(controller.NewController), + + // register router of AiChatFiles module + fx.Provide(NewAiChatFilesRouter), +) + +// init AiChatFilesRouter +func NewAiChatFilesRouter(fiber *fiber.App, controller *controller.Controller) *AiChatFilesRouter { + return &AiChatFilesRouter{ + App: fiber, + Controller: controller, + } +} + +// register routes of AiChatFiles module +func (_i *AiChatFilesRouter) RegisterAiChatFilesRoutes() { + // define controllers + aiChatFilesController := _i.Controller.AiChatFiles + + // define routes + _i.App.Route("/ai-chat-files", func(router fiber.Router) { + router.Get("/", aiChatFilesController.All) + router.Get("/:id", aiChatFilesController.Show) + router.Post("/:messageId", aiChatFilesController.Save) + router.Put("/:id", aiChatFilesController.Update) + router.Delete("/:id", aiChatFilesController.Delete) + router.Get("/viewer/:filename", aiChatFilesController.Viewer) + router.Get("/upload-status/:uploadId", aiChatFilesController.GetUploadStatus) + + }) +} diff --git a/app/module/ai_chat_files/controller/article_files.controller.go b/app/module/ai_chat_files/controller/article_files.controller.go new file mode 100644 index 0000000..988fcc9 --- /dev/null +++ b/app/module/ai_chat_files/controller/article_files.controller.go @@ -0,0 +1,239 @@ +package controller + +import ( + "fmt" + "narasi-ahli-be/app/module/ai_chat_files/request" + "narasi-ahli-be/app/module/ai_chat_files/service" + "narasi-ahli-be/utils/paginator" + utilRes "narasi-ahli-be/utils/response" + utilVal "narasi-ahli-be/utils/validator" + "strconv" + + "github.com/gofiber/fiber/v2" +) + +type aiChatFilesController struct { + aiChatFilesService service.AiChatFilesService +} + +type AiChatFilesController interface { + All(c *fiber.Ctx) error + Show(c *fiber.Ctx) error + Save(c *fiber.Ctx) error + Update(c *fiber.Ctx) error + Delete(c *fiber.Ctx) error + Viewer(c *fiber.Ctx) error + GetUploadStatus(c *fiber.Ctx) error + +} + +func NewAiChatFilesController(aiChatFilesService service.AiChatFilesService) AiChatFilesController { + return &aiChatFilesController{ + aiChatFilesService: aiChatFilesService, + } +} + +// All AIChatFiles +// @Summary Get all AiChatFiles +// @Description API for getting all AiChatFiles +// @Tags AiChat Files +// @Security Bearer +// @Param req query request.AiChatFilesQueryRequest false "query parameters" +// @Param req query paginator.Pagination false "pagination parameters" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /ai-chat-files [get] +func (_i *aiChatFilesController) All(c *fiber.Ctx) error { + // Get from context + paginate, err := paginator.Paginate(c) + if err != nil { + return err + } + + reqContext := request.AiChatFilesQueryRequestContext{ + MessageId: c.Query("messageId"), + FileName: c.Query("fileName"), + } + req := reqContext.ToParamRequest() + req.Pagination = paginate + + aiChatFilesData, paging, err := _i.aiChatFilesService.All(req) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"AiChatFiles list successfully retrieved"}, + Data: aiChatFilesData, + Meta: paging, + }) +} + +// Show AiChatFiles +// @Summary Get one AiChatFiles +// @Description API for getting one AiChatFiles +// @Tags AiChat Files +// @Security Bearer +// @Param id path int true "AiChatFiles ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /ai-chat-files/{id} [get] +func (_i *aiChatFilesController) Show(c *fiber.Ctx) error { + // Get from context + id, err := strconv.ParseUint(c.Params("id"), 10, 0) + if err != nil { + return err + } + + aiChatFilesData, err := _i.aiChatFilesService.Show(uint(id)) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"AiChatFiles successfully retrieved"}, + Data: aiChatFilesData, + }) +} + +// Save AiChatFiles +// @Summary Upload AiChatFiles +// @Description API for create AiChatFiles +// @Tags AiChat Files +// @Security Bearer +// @Produce json +// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token" +// @Param files formData file true "Upload file" multiple true +// @Param aiChatId path int true "AiChat ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /ai-chat-files/{messageId} [post] +func (_i *aiChatFilesController) Save(c *fiber.Ctx) error { + // Get from context + id, err := strconv.ParseUint(c.Params("messageId"), 10, 0) + if err != nil { + return err + } + + err = _i.aiChatFilesService.Save(c, uint(id)) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"AiChatFiles successfully upload"}, + }) +} + +// Update AiChatFiles +// @Summary Update AiChatFiles +// @Description API for update AiChatFiles +// @Tags AiChat Files +// @Security Bearer +// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token" +// @Param payload body request.AiChatFilesUpdateRequest true "Required payload" +// @Param id path int true "AiChatFiles ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /aiChat-files/{id} [put] +func (_i *aiChatFilesController) Update(c *fiber.Ctx) error { + // Get from context + id, err := strconv.ParseUint(c.Params("id"), 10, 0) + if err != nil { + return err + } + + req := new(request.AiChatFilesUpdateRequest) + if err := utilVal.ParseAndValidate(c, req); err != nil { + return err + } + + err = _i.aiChatFilesService.Update(uint(id), *req) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"AiChatFiles successfully updated"}, + }) +} + +// Delete AiChatFiles +// @Summary Delete AiChatFiles +// @Description API for delete AiChatFiles +// @Tags AiChat Files +// @Security Bearer +// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token" +// @Param id path int true "AiChatFiles ID" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /aiChat-files/{id} [delete] +func (_i *aiChatFilesController) Delete(c *fiber.Ctx) error { + // Get from context + id, err := strconv.ParseUint(c.Params("id"), 10, 0) + if err != nil { + return err + } + + err = _i.aiChatFilesService.Delete(uint(id)) + if err != nil { + return err + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"AiChatFiles successfully deleted"}, + }) +} + +// Viewer AiChatFiles +// @Summary Viewer AiChatFiles +// @Description API for Viewer AiChatFiles +// @Tags AiChat Files +// @Security Bearer +// @Param filename path string true "AiChat File Name" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /aiChat-files/viewer/{filename} [get] +func (_i *aiChatFilesController) Viewer(c *fiber.Ctx) error { + // Get from context + return _i.aiChatFilesService.Viewer(c) +} + +// GetUploadStatus AiChatFiles +// @Summary GetUploadStatus AiChatFiles +// @Description API for GetUploadStatus AiChatFiles +// @Tags AiChat Files +// @Security Bearer +// @Param uploadId path string true "Upload ID of AiChatFiles" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /aiChat-files/upload-status/{uploadId} [get] +func (_i *aiChatFilesController) GetUploadStatus(c *fiber.Ctx) error { + progress, _ := _i.aiChatFilesService.GetUploadStatus(c) + progressMessage := fmt.Sprintf("Upload Progress: %d%%", progress) + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{"Upload Status Retrieve"}, + Data: progressMessage, + }) +} diff --git a/app/module/ai_chat_files/controller/controller.go b/app/module/ai_chat_files/controller/controller.go new file mode 100644 index 0000000..dc98632 --- /dev/null +++ b/app/module/ai_chat_files/controller/controller.go @@ -0,0 +1,13 @@ +package controller + +import "narasi-ahli-be/app/module/ai_chat_files/service" + +type Controller struct { + AiChatFiles AiChatFilesController +} + +func NewController(AiChatFilesService service.AiChatFilesService) *Controller { + return &Controller{ + AiChatFiles: NewAiChatFilesController(AiChatFilesService), + } +} diff --git a/app/module/ai_chat_files/mapper/article_files.mapper.go b/app/module/ai_chat_files/mapper/article_files.mapper.go new file mode 100644 index 0000000..488aacc --- /dev/null +++ b/app/module/ai_chat_files/mapper/article_files.mapper.go @@ -0,0 +1,29 @@ +package mapper + +import ( + "narasi-ahli-be/app/database/entity" + res "narasi-ahli-be/app/module/ai_chat_files/response" +) + +func AiChatFilesResponseMapper(aiChatFilesReq *entity.AiChatFiles, host string) (aiChatFilesRes *res.AiChatFilesResponse) { + fileUrl := host + "/ai-chat-files/viewer/" + if aiChatFilesReq.FileName != nil { + fileUrl += *aiChatFilesReq.FileName + } + + if aiChatFilesReq != nil { + aiChatFilesRes = &res.AiChatFilesResponse{ + ID: aiChatFilesReq.ID, + MessageId: aiChatFilesReq.MessageId, + FilePath: aiChatFilesReq.FilePath, + FileUrl: &fileUrl, + FileName: aiChatFilesReq.FileName, + FileAlt: aiChatFilesReq.FileAlt, + Size: aiChatFilesReq.Size, + IsActive: aiChatFilesReq.IsActive, + CreatedAt: aiChatFilesReq.CreatedAt, + UpdatedAt: aiChatFilesReq.UpdatedAt, + } + } + return aiChatFilesRes +} diff --git a/app/module/ai_chat_files/repository/article_files.repository.go b/app/module/ai_chat_files/repository/article_files.repository.go new file mode 100644 index 0000000..e9c01e7 --- /dev/null +++ b/app/module/ai_chat_files/repository/article_files.repository.go @@ -0,0 +1,123 @@ +package repository + +import ( + "fmt" + "narasi-ahli-be/app/database" + "narasi-ahli-be/app/database/entity" + "narasi-ahli-be/app/module/ai_chat_files/request" + "narasi-ahli-be/utils/paginator" + utilSvc "narasi-ahli-be/utils/service" + "strings" +) + +type aiChatFilesRepository struct { + DB *database.Database +} + +// AiChatFilesRepository define interface of IAiChatFilesRepository +type AiChatFilesRepository interface { + GetAll(req request.AiChatFilesQueryRequest) (aiChatFiles []*entity.AiChatFiles, paging paginator.Pagination, err error) + FindOne(id uint) (aiChatFiles *entity.AiChatFiles, err error) + FindByAiChat(messageId uint) (aiChatFiles []*entity.AiChatFiles, err error) + FindByFilename(filename string) (aiChatFiles *entity.AiChatFiles, err error) + Create(aiChatFiles *entity.AiChatFiles) (err error) + Update(id uint, aiChatFiles *entity.AiChatFiles) (err error) + Delete(id uint) (err error) +} + +func NewAiChatFilesRepository(db *database.Database) AiChatFilesRepository { + return &aiChatFilesRepository{ + DB: db, + } +} + +// implement interface of IAiChatFilesRepository +func (_i *aiChatFilesRepository) GetAll(req request.AiChatFilesQueryRequest) (aiChatFiles []*entity.AiChatFiles, paging paginator.Pagination, err error) { + var count int64 + + query := _i.DB.DB.Model(&entity.AiChatFiles{}) + query = query.Where("is_active = ?", true) + + if req.MessageId != nil { + query = query.Where("message_id = ?", req.MessageId) + } + if req.FileName != nil && *req.FileName != "" { + fileName := strings.ToLower(*req.FileName) + query = query.Where("LOWER(file_name) LIKE ?", "%"+strings.ToLower(fileName)+"%") + } + if req.IsPublish != nil { + query = query.Where("is_publish = ?", req.IsPublish) + } + if req.StatusId != nil { + query = query.Where("status_id = ?", req.StatusId) + } + query.Count(&count) + + if req.Pagination.SortBy != "" { + direction := "ASC" + if req.Pagination.Sort == "desc" { + direction = "DESC" + } + query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction)) + } + + req.Pagination.Count = count + req.Pagination = paginator.Paging(req.Pagination) + + err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&aiChatFiles).Error + if err != nil { + return + } + + paging = *req.Pagination + + return +} + +func (_i *aiChatFilesRepository) FindOne(id uint) (aiChatFiles *entity.AiChatFiles, err error) { + query := _i.DB.DB + + if err := query.First(&aiChatFiles, id).Error; err != nil { + return nil, err + } + + return aiChatFiles, nil +} + +func (_i *aiChatFilesRepository) FindByAiChat(messageId uint) (aiChatFiles []*entity.AiChatFiles, err error) { + query := _i.DB.DB.Where("message_id = ?", messageId) + + if err := query.Find(&aiChatFiles).Error; err != nil { + return nil, err + } + + return aiChatFiles, nil +} + +func (_i *aiChatFilesRepository) FindByFilename(aiChatFilename string) (aiChatFiles *entity.AiChatFiles, err error) { + query := _i.DB.DB.Where("file_name = ?", aiChatFilename) + + if err := query.First(&aiChatFiles).Error; err != nil { + return nil, err + } + + return aiChatFiles, nil +} + +func (_i *aiChatFilesRepository) Create(aiChatFiles *entity.AiChatFiles) (err error) { + return _i.DB.DB.Create(aiChatFiles).Error +} + +func (_i *aiChatFilesRepository) Update(id uint, aiChatFiles *entity.AiChatFiles) (err error) { + aiChatFilesMap, err := utilSvc.StructToMap(aiChatFiles) + if err != nil { + return err + } + return _i.DB.DB.Model(&entity.AiChatFiles{}). + Where(&entity.AiChatFiles{ID: id}). + Updates(aiChatFilesMap).Error +} + +func (_i *aiChatFilesRepository) Delete(id uint) error { + return _i.DB.DB.Delete(&entity.AiChatFiles{}, id).Error +} diff --git a/app/module/ai_chat_files/request/ai_chat_files.request.go b/app/module/ai_chat_files/request/ai_chat_files.request.go new file mode 100644 index 0000000..f20ddfd --- /dev/null +++ b/app/module/ai_chat_files/request/ai_chat_files.request.go @@ -0,0 +1,112 @@ +package request + +import ( + "narasi-ahli-be/app/database/entity" + "narasi-ahli-be/utils/paginator" + "strconv" + "time" +) + +type AiChatFilesGeneric interface { + ToEntity() +} + +type AiChatFilesQueryRequest struct { + MessageId *int `json:"messageId"` + FileName *string `json:"fileName"` + StatusId *int `json:"statusId"` + IsPublish *bool `json:"isPublish"` + Pagination *paginator.Pagination `json:"pagination"` +} + +type AiChatFilesCreateRequest struct { + MessageId uint `json:"messageId" validate:"required"` + StatusId int `json:"statusId" validate:"required"` + UploadId *string `json:"uploadId"` + FilePath *string `json:"filePath"` + FileUrl *string `json:"fileUrl"` + FileName *string `json:"fileName"` + FileThumbnail *string `json:"fileThumbnail"` + FileAlt *string `json:"fileAlt"` + WidthPixel *string `json:"widthPixel"` + HeightPixel *string `json:"heightPixel"` + Size *string `json:"size"` +} + +func (req AiChatFilesCreateRequest) ToEntity() *entity.AiChatFiles { + return &entity.AiChatFiles{ + MessageId: req.MessageId, + UploadID: req.UploadId, + FilePath: req.FilePath, + FileUrl: req.FileUrl, + FileName: req.FileName, + FileThumbnail: req.FileThumbnail, + FileAlt: req.FileAlt, + Size: req.Size, + } +} + +type AiChatFilesUpdateRequest struct { + ID uint `json:"id" validate:"required"` + MessageId uint `json:"messageId" validate:"required"` + StatusId int `json:"statusId" validate:"required"` + FilePath *string `json:"filePath"` + FileUrl *string `json:"fileUrl"` + FileName *string `json:"fileName"` + FileThumbnail *string `json:"fileThumbnail"` + FileAlt *string `json:"fileAlt"` + WidthPixel *string `json:"widthPixel"` + HeightPixel *string `json:"heightPixel"` + Size *string `json:"size"` + IsPublish *bool `json:"isPublish" validate:"required"` + PublishedAt *time.Time `json:"publishedAt" validate:"required"` +} + +func (req AiChatFilesUpdateRequest) ToEntity() *entity.AiChatFiles { + return &entity.AiChatFiles{ + ID: req.ID, + MessageId: req.MessageId, + FilePath: req.FilePath, + FileUrl: req.FileUrl, + FileName: req.FileName, + FileThumbnail: req.FileThumbnail, + FileAlt: req.FileAlt, + Size: req.Size, + UpdatedAt: time.Now(), + } +} + +type AiChatFilesQueryRequestContext struct { + MessageId string `json:"messageId"` + FileName string `json:"fileName"` + StatusId string `json:"statusId"` + IsPublish string `json:"isPublish"` +} + +func (req AiChatFilesQueryRequestContext) ToParamRequest() AiChatFilesQueryRequest { + var request AiChatFilesQueryRequest + + if messageIdStr := req.MessageId; messageIdStr != "" { + messageId, err := strconv.Atoi(messageIdStr) + if err == nil { + request.MessageId = &messageId + } + } + if fileName := req.FileName; fileName != "" { + request.FileName = &fileName + } + if statusIdStr := req.StatusId; statusIdStr != "" { + statusId, err := strconv.Atoi(statusIdStr) + if err == nil { + request.StatusId = &statusId + } + } + if isPublishStr := req.IsPublish; isPublishStr != "" { + isPublish, err := strconv.ParseBool(isPublishStr) + if err == nil { + request.IsPublish = &isPublish + } + } + + return request +} diff --git a/app/module/ai_chat_files/response/ai_chat_files.response.go b/app/module/ai_chat_files/response/ai_chat_files.response.go new file mode 100644 index 0000000..4fa4047 --- /dev/null +++ b/app/module/ai_chat_files/response/ai_chat_files.response.go @@ -0,0 +1,17 @@ +package response + +import "time" + +type AiChatFilesResponse struct { + ID uint `json:"id"` + MessageId uint `json:"messageId"` + FilePath *string `json:"filePath"` + FileUrl *string `json:"fileUrl"` + FileName *string `json:"fileName"` + FileAlt *string `json:"fileAlt"` + FileType *string `json:"fileType"` + Size *string `json:"size"` + IsActive bool `json:"isActive"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/app/module/ai_chat_files/service/ai_chat_files.service.go b/app/module/ai_chat_files/service/ai_chat_files.service.go new file mode 100644 index 0000000..30de548 --- /dev/null +++ b/app/module/ai_chat_files/service/ai_chat_files.service.go @@ -0,0 +1,446 @@ +package service + +import ( + "context" + "fmt" + "io" + "log" + "math/rand" + "mime" + "mime/multipart" + "narasi-ahli-be/app/module/ai_chat_files/mapper" + "narasi-ahli-be/app/module/ai_chat_files/repository" + "narasi-ahli-be/app/module/ai_chat_files/request" + "narasi-ahli-be/app/module/ai_chat_files/response" + config "narasi-ahli-be/config/config" + minioStorage "narasi-ahli-be/config/config" + "narasi-ahli-be/utils/paginator" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/minio/minio-go/v7" + "github.com/rs/zerolog" +) + +// AiChatFilesService +type aiChatFilesService struct { + Repo repository.AiChatFilesRepository + Log zerolog.Logger + Cfg *config.Config + MinioStorage *minioStorage.MinioStorage +} + +// AiChatFilesService define interface of IAiChatFilesService +type AiChatFilesService interface { + All(req request.AiChatFilesQueryRequest) (aiChatFiles []*response.AiChatFilesResponse, paging paginator.Pagination, err error) + Show(id uint) (aiChatFiles *response.AiChatFilesResponse, err error) + Save(c *fiber.Ctx, id uint) error + SaveAsync(c *fiber.Ctx, id uint) error + Update(id uint, req request.AiChatFilesUpdateRequest) (err error) + GetUploadStatus(c *fiber.Ctx) (progress int, err error) + Delete(id uint) error + Viewer(c *fiber.Ctx) error +} + +// NewAiChatFilesService init AiChatFilesService +func NewAiChatFilesService(repo repository.AiChatFilesRepository, log zerolog.Logger, cfg *config.Config, minioStorage *minioStorage.MinioStorage) AiChatFilesService { + + return &aiChatFilesService{ + Repo: repo, + Log: log, + Cfg: cfg, + MinioStorage: minioStorage, + } +} + +var ( + progressMap = make(map[string]int) // Menyimpan progress upload per UploadID + progressLock = sync.Mutex{} +) + +type progressWriter struct { + uploadID string + totalSize int64 + uploadedSize *int64 +} + +// All implement interface of AiChatFilesService +func (_i *aiChatFilesService) All(req request.AiChatFilesQueryRequest) (aiChatFiless []*response.AiChatFilesResponse, paging paginator.Pagination, err error) { + results, paging, err := _i.Repo.GetAll(req) + if err != nil { + return + } + + host := _i.Cfg.App.Domain + + for _, result := range results { + aiChatFiless = append(aiChatFiless, mapper.AiChatFilesResponseMapper(result, host)) + } + + return +} + +func (_i *aiChatFilesService) Show(id uint) (aiChatFiles *response.AiChatFilesResponse, err error) { + result, err := _i.Repo.FindOne(id) + if err != nil { + return nil, err + } + + host := _i.Cfg.App.Domain + + return mapper.AiChatFilesResponseMapper(result, host), nil +} + +func (_i *aiChatFilesService) SaveAsync(c *fiber.Ctx, id uint) (err error) { + bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName + + ctx := context.Background() + + form, err := c.MultipartForm() + + if err != nil { + return err + } + //filess := form.File["files"] + + // Create minio connection. + minioClient, err := _i.MinioStorage.ConnectMinio() + if err != nil { + // Return status 500 and minio connection error. + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": true, + "msg": err.Error(), + }) + } + + for _, files := range form.File { + + _i.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "Uploader:: top"). + Interface("files", files).Msg("") + + for _, fileHeader := range files { + _i.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "Uploader:: loop"). + Interface("data", fileHeader).Msg("") + + filename := filepath.Base(fileHeader.Filename) + filenameAlt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))]) + filename = strings.ReplaceAll(filename, " ", "") + filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))]) + extension := filepath.Ext(fileHeader.Filename)[1:] + + now := time.Now() + rand.New(rand.NewSource(now.UnixNano())) + randUniqueId := rand.Intn(1000000) + uploadID := strconv.Itoa(randUniqueId) + + newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId) + newFilename := newFilenameWithoutExt + "." + extension + + objectName := fmt.Sprintf("aiChats/upload/%d/%d/%s", now.Year(), now.Month(), newFilename) + fileSize := strconv.FormatInt(fileHeader.Size, 10) + fileSizeInt := fileHeader.Size + + _i.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "Uploader:: top"). + Interface("Start upload", uploadID).Msg("") + + req := request.AiChatFilesCreateRequest{ + MessageId: id, + UploadId: &uploadID, + FilePath: &objectName, + FileName: &newFilename, + FileAlt: &filenameAlt, + Size: &fileSize, + } + + err = _i.Repo.Create(req.ToEntity()) + if err != nil { + return err + } + + src, err := fileHeader.Open() + if err != nil { + return err + } + defer src.Close() + + tempFilePath := fmt.Sprintf("/tmp/%s", newFilename) + tempFile, err := os.Create(tempFilePath) + if err != nil { + return err + } + defer tempFile.Close() + + // Copy file ke direktori sementara + _, err = io.Copy(tempFile, src) + if err != nil { + return err + } + + go uploadToMinIO(ctx, _i.Log, minioClient, uploadID, tempFilePath, bucketName, objectName, fileSizeInt) + } + } + + _i.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "User:All"). + Interface("data", "Successfully uploaded").Msg("") + + return +} + +func (_i *aiChatFilesService) Save(c *fiber.Ctx, id uint) (err error) { + bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName + + form, err := c.MultipartForm() + + if err != nil { + return err + } + //filess := form.File["files"] + + // Create minio connection. + minioClient, err := _i.MinioStorage.ConnectMinio() + + if err != nil { + // Return status 500 and minio connection error. + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": true, + "msg": err.Error(), + }) + } + + for _, files := range form.File { + + _i.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "Uploader:: top"). + Interface("files", files).Msg("") + + for _, fileHeader := range files { + _i.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "Uploader:: loop"). + Interface("data", fileHeader).Msg("") + + src, err := fileHeader.Open() + if err != nil { + return err + } + defer src.Close() + + filename := filepath.Base(fileHeader.Filename) + filenameAlt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))]) + filename = strings.ReplaceAll(filename, " ", "") + filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))]) + extension := filepath.Ext(fileHeader.Filename)[1:] + + now := time.Now() + rand.New(rand.NewSource(now.UnixNano())) + randUniqueId := rand.Intn(1000000) + + newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId) + newFilename := newFilenameWithoutExt + "." + extension + + objectName := fmt.Sprintf("aiChats/upload/%d/%d/%s", now.Year(), now.Month(), newFilename) + fileSize := strconv.FormatInt(fileHeader.Size, 10) + + req := request.AiChatFilesCreateRequest{ + MessageId: id, + FilePath: &objectName, + FileName: &newFilename, + FileAlt: &filenameAlt, + Size: &fileSize, + } + + err = _i.Repo.Create(req.ToEntity()) + if err != nil { + return err + } + + // Upload file ke MinIO + _, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, fileHeader.Size, minio.PutObjectOptions{}) + if err != nil { + return err + } + } + } + + _i.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "User:All"). + Interface("data", "Successfully uploaded").Msg("") + + return +} + +func (_i *aiChatFilesService) Update(id uint, req request.AiChatFilesUpdateRequest) (err error) { + _i.Log.Info().Interface("data", req).Msg("") + return _i.Repo.Update(id, req.ToEntity()) +} + +func (_i *aiChatFilesService) Delete(id uint) error { + result, err := _i.Repo.FindOne(id) + if err != nil { + return err + } + result.IsActive = false + return _i.Repo.Update(id, result) +} + +func (_i *aiChatFilesService) Viewer(c *fiber.Ctx) (err error) { + filename := c.Params("filename") + result, err := _i.Repo.FindByFilename(filename) + if err != nil { + return err + } + + ctx := context.Background() + bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName + objectName := *result.FilePath + + _i.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "AiChat:Uploads"). + Interface("data", objectName).Msg("") + + // Create minio connection. + minioClient, err := _i.MinioStorage.ConnectMinio() + if err != nil { + // Return status 500 and minio connection error. + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": true, + "msg": err.Error(), + }) + } + + fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{}) + if err != nil { + log.Fatalln(err) + } + defer fileContent.Close() + + // Tentukan Content-Type berdasarkan ekstensi file + contentType := mime.TypeByExtension("." + getFileExtension(objectName)) + if contentType == "" { + contentType = "application/octet-stream" // fallback jika tidak ada tipe MIME yang cocok + } + + c.Set("Content-Type", contentType) + + if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil { + return err + } + + return +} + +func getFileExtension(filename string) string { + // split file name + parts := strings.Split(filename, ".") + + // jika tidak ada ekstensi, kembalikan string kosong + if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") { + return "" + } + + // ambil ekstensi terakhir + return parts[len(parts)-1] +} + +func uploadTempFile(log zerolog.Logger, fileHeader *multipart.FileHeader, filePath string) { + src, err := fileHeader.Open() + if err != nil { + log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "AiChat:uploadToMinIO-0"). + Interface("err", err).Msg("") + } + defer src.Close() + + tempFile, err := os.Create(filePath) + if err != nil { + log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "AiChat:uploadToMinIO-1"). + Interface("err", err).Msg("") + } + defer tempFile.Close() + + // Copy file ke direktori sementara + _, err = io.Copy(tempFile, src) + if err != nil { + log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "AiChat:uploadToMinIO-2"). + Interface("err", err).Msg("") + } +} + +func uploadToMinIO(ctx context.Context, log zerolog.Logger, minioClient *minio.Client, uploadID, filePath, bucketName string, objectName string, fileSize int64) { + file, err := os.Open(filePath) + if err != nil { + log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "AiChat:uploadToMinIO-3"). + Interface("err", err).Msg("") + return + } + defer file.Close() + + // Upload file ke MinIO dengan progress tracking + uploadProgress := int64(0) + reader := io.TeeReader(file, &progressWriter{uploadID: uploadID, totalSize: fileSize, uploadedSize: &uploadProgress}) + + _, err = minioClient.PutObject(ctx, bucketName, objectName, reader, fileSize, minio.PutObjectOptions{}) + if err != nil { + + log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "AiChat:uploadToMinIO-4"). + Interface("err", err).Msg("") + return + } + + // Upload selesai, update progress menjadi 100 + progressLock.Lock() + progressMap[uploadID] = 100 + progressLock.Unlock() + + go removeFileTemp(log, filePath) +} + +func removeFileTemp(log zerolog.Logger, filePath string) { + err := os.Remove(filePath) + if err != nil { + log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "AiChat:uploadToMinIO-5"). + Interface("Failed to remove temporary file", err).Msg("") + } else { + log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "AiChat:uploadToMinIO-6"). + Interface("err", "Temporary file removed").Msg("") + } +} + +func (p *progressWriter) Write(data []byte) (int, error) { + n := len(data) + progressLock.Lock() + defer progressLock.Unlock() + + *p.uploadedSize += int64(n) + progress := int(float64(*p.uploadedSize) / float64(p.totalSize) * 100) + + // Update progress di map + progressMap[p.uploadID] = progress + + return n, nil +} + +func (_i *aiChatFilesService) GetUploadStatus(c *fiber.Ctx) (progress int, err error) { + uploadID := c.Params("uploadId") + + // Ambil progress dari map + progressLock.Lock() + progress, _ = progressMap[uploadID] + progressLock.Unlock() + + return progress, nil +} diff --git a/app/module/ai_chat_files/service/async_uploader.service.go b/app/module/ai_chat_files/service/async_uploader.service.go new file mode 100644 index 0000000..17c627b --- /dev/null +++ b/app/module/ai_chat_files/service/async_uploader.service.go @@ -0,0 +1,139 @@ +package service + +import ( + "context" + "github.com/minio/minio-go/v7" + "github.com/rs/zerolog" + "io" + "time" +) + +// AsyncUploader menangani proses upload secara asynchronous +type UploadService interface { + UploadFile(ctx context.Context, minioClient *minio.Client, uploadID string, reader io.Reader, bucketName string, objectName string, size int64, contentType string) error +} + +type uploadService struct { + uploadManager UploadManager + Log zerolog.Logger +} + +func NewUploadService(uploadManager UploadManager, log zerolog.Logger) UploadService { + return &uploadService{ + uploadManager: uploadManager, + Log: log, + } +} + +func (u *uploadService) UploadFile(ctx context.Context, minioClient *minio.Client, uploadID string, reader io.Reader, bucketName string, objectName string, size int64, contentType string) error { + status := &UploadStatus{ + FileName: objectName, + Size: size, + Progress: 0, + Status: "uploading", + ObjectName: objectName, + BucketName: bucketName, + StartTime: time.Now(), + } + + u.uploadManager.Add(uploadID, status) + + u.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "UploadService::UploadFile"). + Interface("add status", status).Msg("") + + // Upload ke Minio + _, err := minioClient.PutObject( + ctx, + bucketName, + objectName, + reader, + size, + minio.PutObjectOptions{ + ContentType: contentType, + PartSize: 10 * 1024 * 1024, // 10MB part size + }, + ) + + if err != nil { + u.uploadManager.UpdateStatus(uploadID, "error", err) + + u.Log.Info().Str("timestamp", time.Now(). + Format(time.RFC3339)).Str("Service:Resource", "UploadService::UploadFile"). + Interface("error when upload", err).Msg("") + } + + u.uploadManager.UpdateStatus(uploadID, "completed", nil) + return nil +} + +//func (au *UploadService) UploadFile() { +// // Buat context dengan timeout +// ctx, cancel := context.WithTimeout(au.ctx, 30*time.Minute) +// defer cancel() +// +// // Buat reader dari byte slice +// reader := bytes.NewReader(au.fileData) +// pipeReader, pipeWriter := io.Pipe() +// +// au.progressMap.Store(au.uploadID, 0.0) +// +// // Start goroutine to read from reader and write to pipe +// go func() { +// defer pipeWriter.Close() +// buf := make([]byte, au.partSize) +// +// totalParts := int(reader.Size() / au.partSize) +// if reader.Size()%au.partSize != 0 { +// totalParts++ +// } +// +// for i := 0; i < totalParts; i++ { +// n, err := reader.Read(buf) +// if err != nil && err != io.EOF { +// log.Println("Error reading file:", err) +// return +// } +// +// if _, err := pipeWriter.Write(buf[:n]); err != nil { +// log.Println("Error writing to pipe:", err) +// return +// } +// +// progress := float64(i+1) / float64(totalParts) * 100 +// au.progressMap.Store(au.uploadID, progress) +// au.uploadManager.UpdateProgress(au.uploadID, int(progress)) +// } +// }() +// +// // Upload ke Minio +// _, err := au.minioClient.PutObject( +// ctx, +// au.bucketName, +// au.objectName, +// pipeReader, +// reader.Size(), +// minio.PutObjectOptions{ +// ContentType: au.contentType, +// PartSize: 10 * 1024 * 1024, // 10MB part size +// }, +// ) +// +// if err != nil { +// log.Println("Error uploading file:", err) +// au.progressMap.Store(au.uploadID, "error") +// return +// } +// +// fmt.Printf("Uploading process for %s", au.objectName) +// +// if err != nil { +// uploadManager.UpdateStatus(au.uploadID, "error", err) +// fmt.Printf("Upload error for %s: %v\n", au.objectName, err) +// return +// } +// +// au.progressMap.Store(au.uploadID, 100) +// au.uploadManager.UpdateProgress(au.uploadID, 100) +// au.uploadManager.UpdateStatus(au.uploadID, "completed", nil) +//} diff --git a/app/module/ai_chat_files/service/upload_manager.service.go b/app/module/ai_chat_files/service/upload_manager.service.go new file mode 100644 index 0000000..f02ec96 --- /dev/null +++ b/app/module/ai_chat_files/service/upload_manager.service.go @@ -0,0 +1,71 @@ +package service + +import ( + "sync" + "time" +) + +type UploadStatus struct { + FileName string `json:"fileName"` + Size int64 `json:"size"` + Progress int `json:"progress"` + Status string `json:"status"` + ObjectName string `json:"objectName"` + BucketName string `json:"bucketName"` + StartTime time.Time `json:"startTime"` + Error string `json:"error,omitempty"` +} + +type UploadManager interface { + Add(uploadID string, status *UploadStatus) + UpdateProgress(uploadID string, progress int) + UpdateStatus(uploadID string, status string, err error) + Get(uploadID string) (*UploadStatus, bool) +} + +type uploadManager struct { + uploads map[string]*UploadStatus + mutex sync.RWMutex +} + +func NewUploadManager() UploadManager { + return &uploadManager{ + uploads: make(map[string]*UploadStatus), + } +} + +// Add menambahkan status upload baru +func (um *uploadManager) Add(uploadID string, status *UploadStatus) { + um.mutex.Lock() + defer um.mutex.Unlock() + um.uploads[uploadID] = status +} + +// UpdateProgress memperbarui progress upload +func (um *uploadManager) UpdateProgress(uploadID string, progress int) { + um.mutex.Lock() + defer um.mutex.Unlock() + if status, exists := um.uploads[uploadID]; exists { + status.Progress = progress + } +} + +// UpdateStatus memperbarui status upload +func (um *uploadManager) UpdateStatus(uploadID string, status string, err error) { + um.mutex.Lock() + defer um.mutex.Unlock() + if upload, exists := um.uploads[uploadID]; exists { + upload.Status = status + if err != nil { + upload.Error = err.Error() + } + } +} + +// Get mendapatkan status upload berdasarkan ID +func (um *uploadManager) Get(uploadID string) (*UploadStatus, bool) { + um.mutex.RLock() + defer um.mutex.RUnlock() + status, exists := um.uploads[uploadID] + return status, exists +} diff --git a/app/router/api.go b/app/router/api.go index 7b193a0..b6290d3 100644 --- a/app/router/api.go +++ b/app/router/api.go @@ -4,6 +4,7 @@ import ( "narasi-ahli-be/app/module/activity_logs" "narasi-ahli-be/app/module/advertisement" "narasi-ahli-be/app/module/ai_chat" + "narasi-ahli-be/app/module/ai_chat_files" "narasi-ahli-be/app/module/article_approvals" "narasi-ahli-be/app/module/article_categories" "narasi-ahli-be/app/module/article_category_details" @@ -68,6 +69,8 @@ type Router struct { EducationHistoryRouter *education_history.EducationHistoryRouter WorkHistoryRouter *work_history.WorkHistoryRouter ResearchJournalsRouter *research_journals.ResearchJournalsRouter + AIChatFilesRouter *ai_chat_files.AiChatFilesRouter + } func NewRouter( @@ -102,6 +105,7 @@ func NewRouter( educationHistoryRouter *education_history.EducationHistoryRouter, workHistoryRouter *work_history.WorkHistoryRouter, researchJournalsRouter *research_journals.ResearchJournalsRouter, + aiChatFilesRouter *ai_chat_files.AiChatFilesRouter, ) *Router { return &Router{ App: fiber, @@ -134,6 +138,7 @@ func NewRouter( EducationHistoryRouter: educationHistoryRouter, WorkHistoryRouter: workHistoryRouter, ResearchJournalsRouter: researchJournalsRouter, + AIChatFilesRouter: aiChatFilesRouter, } } @@ -176,4 +181,6 @@ func (r *Router) Register() { r.EducationHistoryRouter.RegisterEducationHistoryRoutes() r.WorkHistoryRouter.RegisterWorkHistoryRoutes() r.ResearchJournalsRouter.RegisterResearchJournalsRoutes() + r.AIChatFilesRouter.RegisterAiChatFilesRoutes() + } diff --git a/config/toml/config.toml b/config/toml/config.toml index 0df23f8..4e415cc 100644 --- a/config/toml/config.toml +++ b/config/toml/config.toml @@ -1,7 +1,7 @@ # Configuration vars for cmd/app [app] name = "Fiber starter" -host = "http://38.47.180.165" +host = "http://103.31.38.120" port = ":8800" domain = "https://narasiahli.com/api" external-port = ":8801" @@ -12,7 +12,7 @@ production = false body-limit = 1048576000 # "100 * 1024 * 1024" [db.postgres] -dsn = "postgresql://narasiahli_user:NarasiAhliDB@2025@38.47.180.165:5432/narasiahli_db" # ://:@:/ +dsn = "postgresql://narasiahli_user:NarasiAhliDB@2025@157.10.161.198:5432/narasiahli_db" # ://:@:/ log-mode = "NONE" migrate = true seed = true @@ -24,7 +24,7 @@ level = 0 # panic -> 5, fatal -> 4, error -> 3, warn -> 2, info -> 1, debug -> 0 prettier = true [objectstorage.miniostorage] -endpoint = "38.47.180.165:9009" +endpoint = "is3.cloudhost.id" access-key-id = "lBtjqWidHz1ktBbduwGy" secret-access-key = "nsedJIa2FI7SqsEVcSFqJrlP4JuFRWGLauNpzD0i" use-ssl = false @@ -66,7 +66,7 @@ enable = true retention = 30 [keycloak] -endpoint = "http://38.47.180.165:8008" +endpoint = "http://103.31.38.120:8008" realm = "narasi-ahli" client-id = "narasi-ahli-app" client-secret = "IoU4CkzWkWmg6PrC2Ruh8d76QArb0UzJ" diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index a755ad3..2d58025 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -914,6 +914,223 @@ const docTemplate = `{ } } }, + "/ai-chat-files": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting all AiChatFiles", + "tags": [ + "AiChat Files" + ], + "summary": "Get all AiChatFiles", + "parameters": [ + { + "type": "string", + "name": "fileName", + "in": "query" + }, + { + "type": "boolean", + "name": "isPublish", + "in": "query" + }, + { + "type": "integer", + "name": "messageId", + "in": "query" + }, + { + "type": "integer", + "name": "statusId", + "in": "query" + }, + { + "type": "integer", + "name": "count", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "name": "nextPage", + "in": "query" + }, + { + "type": "integer", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "name": "previousPage", + "in": "query" + }, + { + "type": "string", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "name": "sortBy", + "in": "query" + }, + { + "type": "integer", + "name": "totalPage", + "in": "query" + } + ], + "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" + } + } + } + } + }, + "/ai-chat-files/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting one AiChatFiles", + "tags": [ + "AiChat Files" + ], + "summary": "Get one AiChatFiles", + "parameters": [ + { + "type": "integer", + "description": "AiChatFiles 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" + } + } + } + } + }, + "/ai-chat-files/{messageId}": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for create AiChatFiles", + "produces": [ + "application/json" + ], + "tags": [ + "AiChat Files" + ], + "summary": "Upload AiChatFiles", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "file", + "description": "Upload file", + "name": "files", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "AiChat ID", + "name": "aiChatId", + "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" + } + } + } + } + }, "/ai-chat/logs": { "get": { "security": [ @@ -1700,6 +1917,223 @@ const docTemplate = `{ } } }, + "/aiChat-files/upload-status/{uploadId}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for GetUploadStatus AiChatFiles", + "tags": [ + "AiChat Files" + ], + "summary": "GetUploadStatus AiChatFiles", + "parameters": [ + { + "type": "string", + "description": "Upload ID of AiChatFiles", + "name": "uploadId", + "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" + } + } + } + } + }, + "/aiChat-files/viewer/{filename}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for Viewer AiChatFiles", + "tags": [ + "AiChat Files" + ], + "summary": "Viewer AiChatFiles", + "parameters": [ + { + "type": "string", + "description": "AiChat File Name", + "name": "filename", + "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" + } + } + } + } + }, + "/aiChat-files/{id}": { + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for update AiChatFiles", + "tags": [ + "AiChat Files" + ], + "summary": "Update AiChatFiles", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "description": "Required payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AiChatFilesUpdateRequest" + } + }, + { + "type": "integer", + "description": "AiChatFiles 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" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for delete AiChatFiles", + "tags": [ + "AiChat Files" + ], + "summary": "Delete AiChatFiles", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "integer", + "description": "AiChatFiles 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" + } + } + } + } + }, "/article-approvals": { "get": { "security": [ @@ -15204,6 +15638,57 @@ const docTemplate = `{ } } }, + "request.AiChatFilesUpdateRequest": { + "type": "object", + "required": [ + "id", + "isPublish", + "messageId", + "publishedAt", + "statusId" + ], + "properties": { + "fileAlt": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "fileThumbnail": { + "type": "string" + }, + "fileUrl": { + "type": "string" + }, + "heightPixel": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "isPublish": { + "type": "boolean" + }, + "messageId": { + "type": "integer" + }, + "publishedAt": { + "type": "string" + }, + "size": { + "type": "string" + }, + "statusId": { + "type": "integer" + }, + "widthPixel": { + "type": "string" + } + } + }, "request.ArticleApprovalsCreateRequest": { "type": "object", "required": [ diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 96a8c63..e713a00 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -903,6 +903,223 @@ } } }, + "/ai-chat-files": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting all AiChatFiles", + "tags": [ + "AiChat Files" + ], + "summary": "Get all AiChatFiles", + "parameters": [ + { + "type": "string", + "name": "fileName", + "in": "query" + }, + { + "type": "boolean", + "name": "isPublish", + "in": "query" + }, + { + "type": "integer", + "name": "messageId", + "in": "query" + }, + { + "type": "integer", + "name": "statusId", + "in": "query" + }, + { + "type": "integer", + "name": "count", + "in": "query" + }, + { + "type": "integer", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "name": "nextPage", + "in": "query" + }, + { + "type": "integer", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "name": "previousPage", + "in": "query" + }, + { + "type": "string", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "name": "sortBy", + "in": "query" + }, + { + "type": "integer", + "name": "totalPage", + "in": "query" + } + ], + "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" + } + } + } + } + }, + "/ai-chat-files/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for getting one AiChatFiles", + "tags": [ + "AiChat Files" + ], + "summary": "Get one AiChatFiles", + "parameters": [ + { + "type": "integer", + "description": "AiChatFiles 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" + } + } + } + } + }, + "/ai-chat-files/{messageId}": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for create AiChatFiles", + "produces": [ + "application/json" + ], + "tags": [ + "AiChat Files" + ], + "summary": "Upload AiChatFiles", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "file", + "description": "Upload file", + "name": "files", + "in": "formData", + "required": true + }, + { + "type": "integer", + "description": "AiChat ID", + "name": "aiChatId", + "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" + } + } + } + } + }, "/ai-chat/logs": { "get": { "security": [ @@ -1689,6 +1906,223 @@ } } }, + "/aiChat-files/upload-status/{uploadId}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for GetUploadStatus AiChatFiles", + "tags": [ + "AiChat Files" + ], + "summary": "GetUploadStatus AiChatFiles", + "parameters": [ + { + "type": "string", + "description": "Upload ID of AiChatFiles", + "name": "uploadId", + "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" + } + } + } + } + }, + "/aiChat-files/viewer/{filename}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for Viewer AiChatFiles", + "tags": [ + "AiChat Files" + ], + "summary": "Viewer AiChatFiles", + "parameters": [ + { + "type": "string", + "description": "AiChat File Name", + "name": "filename", + "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" + } + } + } + } + }, + "/aiChat-files/{id}": { + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for update AiChatFiles", + "tags": [ + "AiChat Files" + ], + "summary": "Update AiChatFiles", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "description": "Required payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.AiChatFilesUpdateRequest" + } + }, + { + "type": "integer", + "description": "AiChatFiles 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" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for delete AiChatFiles", + "tags": [ + "AiChat Files" + ], + "summary": "Delete AiChatFiles", + "parameters": [ + { + "type": "string", + "description": "Insert the X-Csrf-Token", + "name": "X-Csrf-Token", + "in": "header", + "required": true + }, + { + "type": "integer", + "description": "AiChatFiles 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" + } + } + } + } + }, "/article-approvals": { "get": { "security": [ @@ -15193,6 +15627,57 @@ } } }, + "request.AiChatFilesUpdateRequest": { + "type": "object", + "required": [ + "id", + "isPublish", + "messageId", + "publishedAt", + "statusId" + ], + "properties": { + "fileAlt": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "fileThumbnail": { + "type": "string" + }, + "fileUrl": { + "type": "string" + }, + "heightPixel": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "isPublish": { + "type": "boolean" + }, + "messageId": { + "type": "integer" + }, + "publishedAt": { + "type": "string" + }, + "size": { + "type": "string" + }, + "statusId": { + "type": "integer" + }, + "widthPixel": { + "type": "string" + } + } + }, "request.ArticleApprovalsCreateRequest": { "type": "object", "required": [ diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 1c60699..3e893cd 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -148,6 +148,41 @@ definitions: - redirectLink - title type: object + request.AiChatFilesUpdateRequest: + properties: + fileAlt: + type: string + fileName: + type: string + filePath: + type: string + fileThumbnail: + type: string + fileUrl: + type: string + heightPixel: + type: string + id: + type: integer + isPublish: + type: boolean + messageId: + type: integer + publishedAt: + type: string + size: + type: string + statusId: + type: integer + widthPixel: + type: string + required: + - id + - isPublish + - messageId + - publishedAt + - statusId + type: object request.ArticleApprovalsCreateRequest: properties: articleId: @@ -1922,6 +1957,142 @@ paths: summary: Viewer Advertisement tags: - Advertisement + /ai-chat-files: + get: + description: API for getting all AiChatFiles + parameters: + - in: query + name: fileName + type: string + - in: query + name: isPublish + type: boolean + - in: query + name: messageId + type: integer + - in: query + name: statusId + type: integer + - in: query + name: count + type: integer + - in: query + name: limit + type: integer + - in: query + name: nextPage + type: integer + - in: query + name: page + type: integer + - in: query + name: previousPage + type: integer + - in: query + name: sort + type: string + - in: query + name: sortBy + type: string + - in: query + name: totalPage + 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: Get all AiChatFiles + tags: + - AiChat Files + /ai-chat-files/{id}: + get: + description: API for getting one AiChatFiles + parameters: + - description: AiChatFiles ID + in: path + name: id + 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: Get one AiChatFiles + tags: + - AiChat Files + /ai-chat-files/{messageId}: + post: + description: API for create AiChatFiles + parameters: + - description: Insert the X-Csrf-Token + in: header + name: X-Csrf-Token + required: true + type: string + - description: Upload file + in: formData + name: files + required: true + type: file + - description: AiChat ID + in: path + name: aiChatId + required: true + type: integer + produces: + - application/json + 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 AiChatFiles + tags: + - AiChat Files /ai-chat/logs: get: description: API for getting all AI chat logs for authenticated user @@ -2424,6 +2595,145 @@ paths: summary: Update AI chat message tags: - AI Chat + /aiChat-files/{id}: + delete: + description: API for delete AiChatFiles + parameters: + - description: Insert the X-Csrf-Token + in: header + name: X-Csrf-Token + required: true + type: string + - description: AiChatFiles ID + in: path + name: id + 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: Delete AiChatFiles + tags: + - AiChat Files + put: + description: API for update AiChatFiles + parameters: + - description: Insert the X-Csrf-Token + in: header + name: X-Csrf-Token + required: true + type: string + - description: Required payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/request.AiChatFilesUpdateRequest' + - description: AiChatFiles ID + in: path + name: id + 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: Update AiChatFiles + tags: + - AiChat Files + /aiChat-files/upload-status/{uploadId}: + get: + description: API for GetUploadStatus AiChatFiles + parameters: + - description: Upload ID of AiChatFiles + in: path + name: uploadId + 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: GetUploadStatus AiChatFiles + tags: + - AiChat Files + /aiChat-files/viewer/{filename}: + get: + description: API for Viewer AiChatFiles + parameters: + - description: AiChat File Name + in: path + name: filename + 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: Viewer AiChatFiles + tags: + - AiChat Files /article-approvals: get: description: API for getting all ArticleApprovals diff --git a/main.go b/main.go index b4ad6f6..27a0a2a 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "narasi-ahli-be/app/module/activity_logs" "narasi-ahli-be/app/module/advertisement" "narasi-ahli-be/app/module/ai_chat" + "narasi-ahli-be/app/module/ai_chat_files" "narasi-ahli-be/app/module/article_approvals" "narasi-ahli-be/app/module/article_categories" "narasi-ahli-be/app/module/article_category_details" @@ -95,6 +96,7 @@ func main() { education_history.NewEducationHistoryModule, work_history.NewWorkHistoryModule, research_journals.NewResearchJournalsModule, + ai_chat_files.NewAiChatFilesModule, // start aplication fx.Invoke(webserver.Start),