From b9fea3ddd0a605c4823779f90f4041e9e50b8aa4 Mon Sep 17 00:00:00 2001 From: hanif salafi Date: Sun, 12 Oct 2025 12:52:06 +0700 Subject: [PATCH] fix: update clients, update articles response --- app/module/articles/mapper/articles.mapper.go | 7 +- app/module/clients/clients.module.go | 3 +- .../clients/controller/clients.controller.go | 55 ++++-- app/module/clients/service/clients.service.go | 56 +++++- docs/CLIENT_LOGO_UPLOAD_API.md | 9 +- docs/CLIENT_UPDATE_WITH_AUTH_API.md | 169 ++++++++++++++++++ docs/swagger/docs.go | 110 ++++++------ docs/swagger/swagger.json | 110 ++++++------ docs/swagger/swagger.yaml | 72 ++++---- 9 files changed, 429 insertions(+), 162 deletions(-) create mode 100644 docs/CLIENT_UPDATE_WITH_AUTH_API.md diff --git a/app/module/articles/mapper/articles.mapper.go b/app/module/articles/mapper/articles.mapper.go index 048f104..9e5a4f5 100644 --- a/app/module/articles/mapper/articles.mapper.go +++ b/app/module/articles/mapper/articles.mapper.go @@ -49,9 +49,9 @@ func ArticlesResponseMapper( } } - categoryName := "" articleCategories, _ := articleCategoryDetailsRepo.FindByArticleId(articlesReq.ID) var articleCategoriesArr []*articleCategoriesResponse.ArticleCategoriesResponse + categoryName := "" if len(articleCategories) > 0 { for _, result := range articleCategories { if result.Category != nil { @@ -59,6 +59,11 @@ func ArticlesResponseMapper( } } log.Info().Interface("articleCategoriesArr", articleCategoriesArr).Msg("") + + // Ambil categoryName dari array pertama + if len(articleCategoriesArr) > 0 { + categoryName = articleCategoriesArr[0].Title + } } articleFiles, _ := articleFilesRepo.FindByArticle(clientId, articlesReq.ID) diff --git a/app/module/clients/clients.module.go b/app/module/clients/clients.module.go index b028ad6..b7fca56 100644 --- a/app/module/clients/clients.module.go +++ b/app/module/clients/clients.module.go @@ -53,10 +53,11 @@ func (_i *ClientsRouter) RegisterClientsRoutes() { router.Post("/", clientsController.Save) router.Post("/with-user", clientsController.CreateClientWithUser) router.Put("/:id", clientsController.Update) + router.Put("/update", clientsController.UpdateWithAuth) router.Delete("/:id", clientsController.Delete) // Logo upload routes - router.Post("/:id/logo", clientsController.UploadLogo) + router.Post("/logo", clientsController.UploadLogo) router.Delete("/:id/logo", clientsController.DeleteLogo) router.Get("/:id/logo/url", clientsController.GetLogoURL) }) diff --git a/app/module/clients/controller/clients.controller.go b/app/module/clients/controller/clients.controller.go index 694693a..d2d421d 100644 --- a/app/module/clients/controller/clients.controller.go +++ b/app/module/clients/controller/clients.controller.go @@ -23,6 +23,7 @@ type ClientsController interface { Show(c *fiber.Ctx) error Save(c *fiber.Ctx) error Update(c *fiber.Ctx) error + UpdateWithAuth(c *fiber.Ctx) error Delete(c *fiber.Ctx) error // New hierarchy endpoints @@ -205,6 +206,42 @@ func (_i *clientsController) Update(c *fiber.Ctx) error { }) } +// UpdateWithAuth update Clients using auth token +// @Summary update Clients with auth token +// @Description API for update Clients using client ID from auth token +// @Tags Clients +// @Security Bearer +// @Param Authorization header string false "Insert your access token" default(Bearer ) +// @Param payload body request.ClientsUpdateRequest true "Required payload" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /clients/update [put] +func (_i *clientsController) UpdateWithAuth(c *fiber.Ctx) error { + // Get Authorization token from header + authToken := c.Get("Authorization") + _i.Log.Info().Str("authToken", authToken).Msg("") + + req := new(request.ClientsUpdateRequest) + if err := utilVal.ParseAndValidate(c, req); err != nil { + return err + } + + err := _i.clientsService.UpdateWithAuth(authToken, *req) + 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{"Clients successfully updated"}, + }) +} + // Delete delete Clients // @Summary delete Clients // @Description API for delete Clients @@ -498,26 +535,22 @@ func (_i *clientsController) CreateClientWithUser(c *fiber.Ctx) error { // UploadLogo uploads client logo // @Summary Upload client logo -// @Description API for uploading client logo image to MinIO +// @Description API for uploading client logo image to MinIO (uses client ID from auth token) // @Tags Clients // @Security Bearer -// @Param id path string true "Client ID" +// @Param Authorization header string false "Insert your access token" default(Bearer ) // @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] +// @Router /clients/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"}, - }) - } + // Get Authorization token from header + authToken := c.Get("Authorization") + _i.Log.Info().Str("authToken", authToken).Msg("") - imagePath, err := _i.clientsService.UploadLogo(clientId, c) + imagePath, err := _i.clientsService.UploadLogo(authToken, c) if err != nil { return utilRes.Resp(c, utilRes.Response{ Success: false, diff --git a/app/module/clients/service/clients.service.go b/app/module/clients/service/clients.service.go index d5fd956..f194b12 100644 --- a/app/module/clients/service/clients.service.go +++ b/app/module/clients/service/clients.service.go @@ -36,6 +36,7 @@ type ClientsService interface { Show(id uuid.UUID) (clients *response.ClientsResponse, err error) Save(req request.ClientsCreateRequest, authToken string) (clients *entity.Clients, err error) Update(id uuid.UUID, req request.ClientsUpdateRequest) (err error) + UpdateWithAuth(authToken string, req request.ClientsUpdateRequest) (err error) Delete(id uuid.UUID) error // New hierarchy methods @@ -49,7 +50,7 @@ type ClientsService interface { CreateClientWithUser(req request.ClientWithUserCreateRequest) (*response.ClientWithUserResponse, error) // Logo upload methods - UploadLogo(clientId uuid.UUID, c *fiber.Ctx) (string, error) + UploadLogo(authToken string, c *fiber.Ctx) (string, error) DeleteLogo(clientId uuid.UUID, imagePath string) error GetLogoURL(imagePath string) (string, error) } @@ -148,6 +149,44 @@ func (_i *clientsService) Update(id uuid.UUID, req request.ClientsUpdateRequest) return _i.Repo.Update(id, updateReq) } +func (_i *clientsService) UpdateWithAuth(authToken string, req request.ClientsUpdateRequest) (err error) { + _i.Log.Info().Interface("data", req).Msg("") + + // Extract clientId from authToken + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user == nil { + _i.Log.Error().Msg("User not found from auth token") + return fmt.Errorf("user not found") + } + + if user.ClientId == nil { + _i.Log.Error().Msg("Client ID not found in user token") + return fmt.Errorf("client ID not found in user token") + } + + clientId := *user.ClientId + _i.Log.Info().Str("clientId", clientId.String()).Msg("Updating client with auth token") + + // Convert request to entity + updateReq := &entity.Clients{ + Name: *req.Name, + 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, + IsActive: req.IsActive, + } + + return _i.Repo.Update(clientId, updateReq) +} + func (_i *clientsService) Delete(id uuid.UUID) error { result, err := _i.Repo.FindOne(id) if err != nil { @@ -435,7 +474,20 @@ func (_i *clientsService) CreateClientWithUser(req request.ClientWithUserCreateR // ===================================================================== // UploadLogo uploads client logo to MinIO -func (_i *clientsService) UploadLogo(clientId uuid.UUID, c *fiber.Ctx) (string, error) { +func (_i *clientsService) UploadLogo(authToken string, c *fiber.Ctx) (string, error) { + // Extract clientId from authToken + user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) + if user == nil { + _i.Log.Error().Msg("User not found from auth token") + return "", fmt.Errorf("user not found") + } + + if user.ClientId == nil { + _i.Log.Error().Msg("Client ID not found in user token") + return "", fmt.Errorf("client ID not found in user token") + } + + clientId := *user.ClientId _i.Log.Info().Str("clientId", clientId.String()).Msg("Uploading client logo") // Upload logo using the upload service diff --git a/docs/CLIENT_LOGO_UPLOAD_API.md b/docs/CLIENT_LOGO_UPLOAD_API.md index 7aa132b..df398b8 100644 --- a/docs/CLIENT_LOGO_UPLOAD_API.md +++ b/docs/CLIENT_LOGO_UPLOAD_API.md @@ -8,15 +8,16 @@ API untuk mengelola logo client dengan integrasi MinIO storage. Mendukung upload ### 1. Upload Client Logo -**POST** `/clients/{id}/logo` +**POST** `/clients/logo` -Upload logo image untuk client tertentu. +Upload logo image untuk client yang sedang login (menggunakan client ID dari auth token). #### Request - **Content-Type**: `multipart/form-data` - **Parameter**: - - `id` (path): Client ID (UUID) - `logo` (form-data): File image (jpg, jpeg, png, gif, webp, max 5MB) +- **Headers**: + - `Authorization`: Bearer token (required) #### Response ```json @@ -32,7 +33,7 @@ Upload logo image untuk client tertentu. #### Example cURL ```bash curl -X POST \ - http://localhost:8080/clients/123e4567-e89b-12d3-a456-426614174000/logo \ + http://localhost:8080/clients/logo \ -H "Authorization: Bearer your-token" \ -F "logo=@/path/to/logo.png" ``` diff --git a/docs/CLIENT_UPDATE_WITH_AUTH_API.md b/docs/CLIENT_UPDATE_WITH_AUTH_API.md new file mode 100644 index 0000000..98f24b9 --- /dev/null +++ b/docs/CLIENT_UPDATE_WITH_AUTH_API.md @@ -0,0 +1,169 @@ +# Client Update with Auth Token API Documentation + +## Overview +API endpoint untuk mengupdate data client menggunakan client ID yang diambil dari auth token, tanpa perlu menyertakan client ID sebagai path parameter. + +## Endpoint + +### Update Client with Auth Token +**PUT** `/clients/update` + +#### Description +Mengupdate data client yang sedang login menggunakan client ID dari auth token. + +#### Parameters +- Tidak ada path parameter, client ID diambil dari auth token + +#### Headers +- **Authorization** (required): Bearer token untuk autentikasi user + +#### Request Body +```json +{ + "name": "Updated Client Name", + "description": "Updated client description", + "clientType": "standalone", + "parentClientId": null, + "maxUsers": 100, + "maxStorage": 1073741824, + "settings": "{\"theme\": \"dark\"}", + "isActive": true, + "logoUrl": "https://example.com/logo.png", + "logoImagePath": "clients/logos/client-id/logo.png", + "address": "Jl. Example No. 123", + "phoneNumber": "+62-123-456-7890", + "website": "https://example.com" +} +``` + +#### Response Fields +- **name** (string, optional): Nama client +- **description** (string, optional): Deskripsi client +- **clientType** (string, optional): Tipe client (`parent_client`, `sub_client`, `standalone`) +- **parentClientId** (string, optional): ID parent client (untuk sub client) +- **maxUsers** (integer, optional): Batas maksimal user +- **maxStorage** (integer, optional): Batas maksimal storage dalam bytes +- **settings** (string, optional): JSON string untuk custom settings +- **isActive** (boolean, optional): Status aktif client +- **logoUrl** (string, optional): URL logo client +- **logoImagePath** (string, optional): Path logo di MinIO storage +- **address** (string, optional): Alamat client +- **phoneNumber** (string, optional): Nomor telepon client +- **website** (string, optional): Website resmi client + +#### Response + +##### Success Response (200) +```json +{ + "success": true, + "messages": ["Clients successfully updated"], + "data": null +} +``` + +#### Error Responses + +##### 400 Bad Request +```json +{ + "success": false, + "messages": ["Validation error"], + "data": { + "field": "error message" + } +} +``` + +##### 401 Unauthorized +```json +{ + "success": false, + "messages": ["user not found"] +} +``` + +##### 500 Internal Server Error +```json +{ + "success": false, + "messages": ["client ID not found in user token"] +} +``` + +## Usage Examples + +### cURL Example +```bash +curl -X PUT "http://localhost:8080/clients/update" \ + -H "Authorization: Bearer YOUR_TOKEN_HERE" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Updated Client Name", + "description": "Updated description", + "clientType": "standalone", + "maxUsers": 100, + "isActive": true, + "address": "Jl. Example No. 123", + "phoneNumber": "+62-123-456-7890", + "website": "https://example.com" + }' +``` + +### JavaScript Example +```javascript +const updateClient = async (clientData) => { + try { + const response = await fetch('/clients/update', { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(clientData) + }); + + const data = await response.json(); + + if (data.success) { + console.log('Client updated successfully'); + } else { + console.error('Update failed:', data.messages); + } + } catch (error) { + console.error('Error updating client:', error); + } +}; + +// Usage +updateClient({ + name: "Updated Client Name", + description: "Updated description", + clientType: "standalone", + maxUsers: 100, + isActive: true, + address: "Jl. Example No. 123", + phoneNumber: "+62-123-456-7890", + website: "https://example.com" +}); +``` + +## Use Cases +1. **Profile Update**: User mengupdate profil client mereka sendiri +2. **Settings Management**: Mengubah pengaturan client +3. **Contact Information**: Mengupdate informasi kontak client +4. **Logo Management**: Mengupdate URL atau path logo client +5. **Resource Limits**: Mengubah batas user atau storage + +## Security Features +- **Authentication Required**: Harus menggunakan Bearer token yang valid +- **Client Isolation**: User hanya bisa mengupdate client mereka sendiri +- **Token Validation**: Client ID diambil dari token yang sudah diverifikasi +- **Input Validation**: Semua input divalidasi sebelum diproses + +## Notes +- Endpoint ini menggunakan middleware `UserMiddleware` untuk mengekstrak informasi user dari JWT token +- Client ID diambil dari `user.ClientId` dalam token +- Jika user tidak ditemukan atau client ID tidak ada dalam token, akan mengembalikan error +- Semua field dalam request body bersifat optional +- Endpoint ini lebih aman daripada endpoint update dengan path parameter karena mencegah user mengupdate client lain diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 2e6b1f7..a035f01 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -9744,6 +9744,62 @@ const docTemplate = `{ } } }, + "/clients/logo": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for uploading client logo image to MinIO (uses client ID from auth token)", + "tags": [ + "Clients" + ], + "summary": "Upload client logo", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "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" + } + } + } + } + }, "/clients/with-user": { "post": { "description": "API for creating a client and its admin user in a single request (Public endpoint)", @@ -9986,60 +10042,6 @@ 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": [ { diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 5640b50..92a1d7f 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -9733,6 +9733,62 @@ } } }, + "/clients/logo": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "API for uploading client logo image to MinIO (uses client ID from auth token)", + "tags": [ + "Clients" + ], + "summary": "Upload client logo", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header" + }, + { + "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" + } + } + } + } + }, "/clients/with-user": { "post": { "description": "API for creating a client and its admin user in a single request (Public endpoint)", @@ -9975,60 +10031,6 @@ } }, "/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": [ { diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index fa2e121..c70ae32 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -8042,41 +8042,6 @@ paths: 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 @@ -8275,6 +8240,43 @@ paths: summary: Bulk create sub-clients tags: - Clients + /clients/logo: + post: + description: API for uploading client logo image to MinIO (uses client ID from + auth token) + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + 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/with-user: post: description: API for creating a client and its admin user in a single request