diff --git a/app/module/clients/clients.module.go b/app/module/clients/clients.module.go index dcfa7e6..1e7fd19 100644 --- a/app/module/clients/clients.module.go +++ b/app/module/clients/clients.module.go @@ -50,6 +50,7 @@ func (_i *ClientsRouter) RegisterClientsRoutes() { _i.App.Route("/clients", func(router fiber.Router) { router.Get("/", clientsController.All) router.Get("/profile", clientsController.ShowWithAuth) + router.Get("/check-name/:name", clientsController.CheckClientNameExists) router.Get("/:id", clientsController.Show) router.Post("/", clientsController.Save) router.Post("/with-user", clientsController.CreateClientWithUser) @@ -61,5 +62,6 @@ func (_i *ClientsRouter) RegisterClientsRoutes() { router.Post("/logo", clientsController.UploadLogo) router.Delete("/:id/logo", clientsController.DeleteLogo) router.Get("/:id/logo/url", clientsController.GetLogoURL) + router.Get("/logo/:filename", clientsController.ViewLogo) }) } diff --git a/app/module/clients/controller/clients.controller.go b/app/module/clients/controller/clients.controller.go index 17fe170..667513e 100644 --- a/app/module/clients/controller/clients.controller.go +++ b/app/module/clients/controller/clients.controller.go @@ -22,6 +22,7 @@ type ClientsController interface { All(c *fiber.Ctx) error Show(c *fiber.Ctx) error ShowWithAuth(c *fiber.Ctx) error + CheckClientNameExists(c *fiber.Ctx) error Save(c *fiber.Ctx) error Update(c *fiber.Ctx) error UpdateWithAuth(c *fiber.Ctx) error @@ -42,6 +43,7 @@ type ClientsController interface { UploadLogo(c *fiber.Ctx) error DeleteLogo(c *fiber.Ctx) error GetLogoURL(c *fiber.Ctx) error + ViewLogo(c *fiber.Ctx) error } func NewClientsController(clientsService service.ClientsService, log zerolog.Logger) ClientsController { @@ -172,6 +174,41 @@ func (_i *clientsController) ShowWithAuth(c *fiber.Ctx) error { }) } +// CheckClientNameExists check if client name exists +// @Summary Check if client name exists +// @Description API for checking if client name exists (returns only exist status) +// @Tags Clients +// @Param name path string true "Client name to check" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 500 {object} response.InternalServerError +// @Router /clients/check-name/{name} [get] +func (_i *clientsController) CheckClientNameExists(c *fiber.Ctx) error { + name := c.Params("name") + + exists, err := _i.clientsService.CheckClientNameExists(name) + if err != nil { + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{err.Error()}, + }) + } + + message := "Client name is available" + if exists { + message = "Name has been used" + } + + return utilRes.Resp(c, utilRes.Response{ + Success: true, + Messages: utilRes.Messages{message}, + Data: map[string]interface{}{ + "name": name, + "exists": exists, + }, + }) +} + // Save create Clients // @Summary Create Clients // @Description API for create Clients @@ -701,3 +738,35 @@ func (_i *clientsController) GetLogoURL(c *fiber.Ctx) error { }, }) } + +// ViewLogo serves client logo file +// @Summary View client logo +// @Description API for viewing client logo file by filename +// @Tags Clients +// @Param filename path string true "Logo filename" +// @Success 200 {file} file +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 401 {object} response.UnauthorizedError +// @Failure 500 {object} response.InternalServerError +// @Router /clients/logo/{filename} [get] +func (_i *clientsController) ViewLogo(c *fiber.Ctx) error { + filename := c.Params("filename") + _i.Log.Info().Str("filename", filename).Msg("Viewing client logo") + + // Get logo file from MinIO + data, contentType, err := _i.clientsService.ViewLogo(filename) + if err != nil { + _i.Log.Error().Err(err).Str("filename", filename).Msg("Failed to get logo file") + return utilRes.Resp(c, utilRes.Response{ + Success: false, + Messages: utilRes.Messages{err.Error()}, + }) + } + + // Set content type and serve file + c.Set("Content-Type", contentType) + c.Set("Cache-Control", "public, max-age=3600") // Cache for 1 hour + + return c.Send(data) +} diff --git a/app/module/clients/repository/clients.repository.go b/app/module/clients/repository/clients.repository.go index 5c08c33..b9b087a 100644 --- a/app/module/clients/repository/clients.repository.go +++ b/app/module/clients/repository/clients.repository.go @@ -24,6 +24,8 @@ 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) + FindByName(name string) (clients *entity.Clients, err error) + FindByImagePathName(name string) (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) @@ -150,6 +152,22 @@ func (_i *clientsRepository) FindOneByClientId(clientId *uuid.UUID) (clients *en return clients, nil } +func (_i *clientsRepository) FindByName(name string) (clients *entity.Clients, err error) { + if err := _i.DB.DB.Where("name = ?", name).First(&clients).Error; err != nil { + return nil, err + } + + return clients, nil +} + +func (_i *clientsRepository) FindByImagePathName(name string) (clients *entity.Clients, err error) { + if err := _i.DB.DB.Where("logo_image_path like ?", "%"+name+"%").First(&clients).Error; err != nil { + return nil, err + } + + return clients, nil +} + func (_i *clientsRepository) Create(clients *entity.Clients) (clientsReturn *entity.Clients, err error) { result := _i.DB.DB.Create(clients) return clients, result.Error diff --git a/app/module/clients/request/clients.request.go b/app/module/clients/request/clients.request.go index 9e5f881..2bca19a 100644 --- a/app/module/clients/request/clients.request.go +++ b/app/module/clients/request/clients.request.go @@ -1,6 +1,7 @@ package request import ( + "netidhub-saas-be/app/database/entity" "netidhub-saas-be/utils/paginator" "github.com/google/uuid" @@ -76,6 +77,54 @@ type ClientsUpdateRequest struct { Website *string `json:"website"` // Website resmi } +// ToEntity converts ClientsUpdateRequest to entity.Clients +func (req *ClientsUpdateRequest) ToEntity() *entity.Clients { + entity := &entity.Clients{} + + // Only set fields that are provided (not nil) + if req.Name != nil { + entity.Name = *req.Name + } + if req.Description != nil { + entity.Description = req.Description + } + if req.ClientType != nil { + entity.ClientType = *req.ClientType + } + if req.ParentClientId != nil { + entity.ParentClientId = req.ParentClientId + } + if req.MaxUsers != nil { + entity.MaxUsers = req.MaxUsers + } + if req.MaxStorage != nil { + entity.MaxStorage = req.MaxStorage + } + if req.Settings != nil { + entity.Settings = req.Settings + } + if req.IsActive != nil { + entity.IsActive = req.IsActive + } + if req.LogoUrl != nil { + entity.LogoUrl = req.LogoUrl + } + if req.LogoImagePath != nil { + entity.LogoImagePath = req.LogoImagePath + } + if req.Address != nil { + entity.Address = req.Address + } + if req.PhoneNumber != nil { + entity.PhoneNumber = req.PhoneNumber + } + if req.Website != nil { + entity.Website = req.Website + } + + return entity +} + // ClientsQueryRequest for filtering and pagination type ClientsQueryRequest struct { // Search filters diff --git a/app/module/clients/service/client_logo_upload.service.go b/app/module/clients/service/client_logo_upload.service.go index 0b1e16d..51a62f9 100644 --- a/app/module/clients/service/client_logo_upload.service.go +++ b/app/module/clients/service/client_logo_upload.service.go @@ -3,7 +3,7 @@ package service import ( "context" "fmt" - "math/rand" + "io" "path/filepath" "strings" "time" @@ -30,18 +30,29 @@ func NewClientLogoUploadService(minioStorage *config.MinioStorage, log zerolog.L } // UploadLogo uploads client logo to MinIO and returns the image path -func (s *ClientLogoUploadService) UploadLogo(c *fiber.Ctx, clientId string) (string, error) { +func (s *ClientLogoUploadService) UploadLogo(c *fiber.Ctx, clientId, clientName string) (string, error) { + + s.Log.Info(). + Str("UploadLogo", clientId). + Msg("Client upload logo process") + // Get the uploaded file file, err := c.FormFile("logo") if err != nil { return "", fmt.Errorf("failed to get uploaded file: %w", err) } + s.Log.Info(). + Str("Upload Logo File", clientId).Interface("file", file).Msg("Client upload logo files") + // Validate file type if !s.isValidImageType(file.Filename) { return "", fmt.Errorf("invalid file type. Only jpg, jpeg, png, gif, webp are allowed") } + s.Log.Info(). + Str("Upload Logo File", clientId).Interface("file", s.isValidImageType(file.Filename)).Msg("Client upload logo files") + // Validate file size (max 5MB) const maxSize = 5 * 1024 * 1024 // 5MB if file.Size > maxSize { @@ -56,8 +67,11 @@ func (s *ClientLogoUploadService) UploadLogo(c *fiber.Ctx, clientId string) (str bucketName := s.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName - // Generate unique filename - filename := s.generateUniqueFilename(file.Filename) + s.Log.Info(). + Str("Upload Logo bucketName", clientId).Interface("bucketName", bucketName).Msg("Client upload logo files") + + // Generate unique filename using client name + filename := s.generateUniqueFilename(file.Filename, clientName) objectName := fmt.Sprintf("clients/logos/%s/%s", clientId, filename) // Open file @@ -143,6 +157,46 @@ func (s *ClientLogoUploadService) GetLogoURL(imagePath string, expiry time.Durat return url.String(), nil } +// GetLogoFile retrieves logo file from MinIO and returns file data and content type +func (s *ClientLogoUploadService) GetLogoFile(clientId, filename string) ([]byte, string, error) { + if filename == "" { + return nil, "", fmt.Errorf("filename is required") + } + + // Create MinIO connection + minioClient, err := s.MinioStorage.ConnectMinio() + if err != nil { + return nil, "", fmt.Errorf("failed to connect to MinIO: %w", err) + } + + bucketName := s.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName + + objectName := fmt.Sprintf("clients/logos/%s/%s", clientId, filename) + // Get object from MinIO + object, err := minioClient.GetObject(context.Background(), bucketName, objectName, minio.GetObjectOptions{}) + if err != nil { + return nil, "", fmt.Errorf("failed to get object from MinIO: %w", err) + } + defer object.Close() + + // Read object data + data, err := io.ReadAll(object) + if err != nil { + return nil, "", fmt.Errorf("failed to read object data: %w", err) + } + + // Get content type based on file extension + contentType := s.getContentType(filename) + + s.Log.Info(). + Str("filename", filename). + Str("contentType", contentType). + Int("dataSize", len(data)). + Msg("Logo file retrieved successfully") + + return data, contentType, 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)) @@ -156,26 +210,56 @@ func (s *ClientLogoUploadService) isValidImageType(filename string) bool { return false } -// generateUniqueFilename generates a unique filename with timestamp and random number -func (s *ClientLogoUploadService) generateUniqueFilename(originalFilename string) string { +// generateUniqueFilename generates a unique filename using client name slug + Unix timestamp +func (s *ClientLogoUploadService) generateUniqueFilename(originalFilename, clientName 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, "-", "_") + // Create slug from client name + clientSlug := s.createSlug(clientName) - // Generate unique suffix + // Get Unix timestamp now := time.Now() - rand.Seed(now.UnixNano()) - randomNum := rand.Intn(1000000) + timestamp := now.Unix() - // Format: originalname_timestamp_random.ext - return fmt.Sprintf("%s_%d_%d%s", - nameWithoutExt, - now.Unix(), - randomNum, - ext) + // Format: clientname_unix_timestamp.ext + return fmt.Sprintf("%s_%d%s", clientSlug, timestamp, ext) +} + +// createSlug converts client name to URL-friendly slug +func (s *ClientLogoUploadService) createSlug(name string) string { + // Convert to lowercase + slug := strings.ToLower(name) + + // Replace spaces with underscores + slug = strings.ReplaceAll(slug, " ", "_") + + // Replace hyphens with underscores + slug = strings.ReplaceAll(slug, "-", "_") + + // Remove special characters (keep only alphanumeric and underscores) + var result strings.Builder + for _, char := range slug { + if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '_' { + result.WriteRune(char) + } + } + + slug = result.String() + + // Remove multiple consecutive underscores + for strings.Contains(slug, "__") { + slug = strings.ReplaceAll(slug, "__", "_") + } + + // Remove leading/trailing underscores + slug = strings.Trim(slug, "_") + + // If empty after cleaning, use "client" + if slug == "" { + slug = "client" + } + + return slug } // getContentType returns the MIME type based on file extension diff --git a/app/module/clients/service/clients.service.go b/app/module/clients/service/clients.service.go index 6d9df80..0a5ea48 100644 --- a/app/module/clients/service/clients.service.go +++ b/app/module/clients/service/clients.service.go @@ -12,6 +12,7 @@ import ( usersRequest "netidhub-saas-be/app/module/users/request" usersService "netidhub-saas-be/app/module/users/service" "netidhub-saas-be/utils/paginator" + "strings" "time" "github.com/gofiber/fiber/v2" @@ -34,6 +35,7 @@ type clientsService struct { type ClientsService interface { All(authToken string, req request.ClientsQueryRequest) (clients []*response.ClientsResponse, paging paginator.Pagination, err error) Show(id uuid.UUID) (clients *response.ClientsResponse, err error) + CheckClientNameExists(name string) (exists bool, err error) ShowWithAuth(authToken string) (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) @@ -54,6 +56,7 @@ type ClientsService interface { UploadLogo(authToken string, c *fiber.Ctx) (string, error) DeleteLogo(clientId uuid.UUID, imagePath string) error GetLogoURL(imagePath string) (string, error) + ViewLogo(filename string) ([]byte, string, error) } // NewClientsService init ClientsService @@ -101,6 +104,28 @@ func (_i *clientsService) Show(id uuid.UUID) (clients *response.ClientsResponse, return mapper.ClientsResponseMapper(result), nil } +func (_i *clientsService) CheckClientNameExists(name string) (exists bool, err error) { + _i.Log.Info().Str("name", name).Msg("Checking if client name exists") + + // Check if client name exists in repository + result, err := _i.Repo.FindByName(name) + if err != nil { + // If error is "record not found", name doesn't exist + if strings.Contains(err.Error(), "record not found") { + return false, nil + } + _i.Log.Error().Err(err).Str("name", name).Msg("Failed to check client name existence") + return false, err + } + + // If result is not nil, name exists + if result != nil { + return true, nil + } + + return false, nil +} + func (_i *clientsService) ShowWithAuth(authToken string) (clients *response.ClientsResponse, err error) { // Extract clientId from authToken user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken) @@ -517,19 +542,29 @@ func (_i *clientsService) UploadLogo(authToken string, c *fiber.Ctx) (string, er clientId := *user.ClientId _i.Log.Info().Str("clientId", clientId.String()).Msg("Uploading client logo") + // Fetch client data to get client name + client, err := _i.Repo.FindOne(clientId) + if err != nil { + _i.Log.Error().Err(err).Str("clientId", clientId.String()).Msg("Failed to fetch client data") + return "", fmt.Errorf("failed to fetch client data: %w", err) + } + // Upload logo using the upload service - imagePath, err := _i.ClientLogoUploadSvc.UploadLogo(c, clientId.String()) + imagePath, err := _i.ClientLogoUploadSvc.UploadLogo(c, clientId.String(), client.Name) if err != nil { _i.Log.Error().Err(err).Str("clientId", clientId.String()).Msg("Failed to upload client logo") return "", err } + _i.Log.Info(). + Str("Upload imagePath", imagePath).Msg("Client upload logo files") + // Update client with new logo image path updateReq := request.ClientsUpdateRequest{ LogoImagePath: &imagePath, } - err = _i.Update(clientId, updateReq) + err = _i.Repo.Update(clientId, updateReq.ToEntity()) if err != nil { _i.Log.Error().Err(err).Str("clientId", clientId.String()).Str("imagePath", imagePath).Msg("Failed to update client with logo path") @@ -562,7 +597,7 @@ func (_i *clientsService) DeleteLogo(clientId uuid.UUID, imagePath string) error LogoImagePath: &emptyPath, } - err = _i.Update(clientId, updateReq) + err = _i.Repo.Update(clientId, updateReq.ToEntity()) 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) @@ -590,3 +625,23 @@ func (_i *clientsService) GetLogoURL(imagePath string) (string, error) { _i.Log.Info().Str("imagePath", imagePath).Str("url", url).Msg("Logo URL generated successfully") return url, nil } + +// ViewLogo retrieves logo file from MinIO +func (_i *clientsService) ViewLogo(filename string) ([]byte, string, error) { + _i.Log.Info().Str("filename", filename).Msg("Retrieving logo file") + + client, err := _i.Repo.FindByImagePathName(filename) + if err != nil { + _i.Log.Error().Err(err).Str("filename", filename).Msg("Failed to find client by image path name") + return nil, "", err + } + + data, contentType, err := _i.ClientLogoUploadSvc.GetLogoFile(client.ID.String(), filename) + if err != nil { + _i.Log.Error().Err(err).Str("filename", filename).Msg("Failed to retrieve logo file") + return nil, "", err + } + + _i.Log.Info().Str("filename", filename).Str("contentType", contentType).Int("dataSize", len(data)).Msg("Logo file retrieved successfully") + return data, contentType, nil +} diff --git a/app/module/users/controller/users.controller.go b/app/module/users/controller/users.controller.go index b2a5fb9..369cb91 100644 --- a/app/module/users/controller/users.controller.go +++ b/app/module/users/controller/users.controller.go @@ -7,7 +7,6 @@ import ( "strconv" "github.com/gofiber/fiber/v2" - _ "github.com/gofiber/fiber/v2" utilRes "netidhub-saas-be/utils/response" utilVal "netidhub-saas-be/utils/validator" @@ -21,6 +20,7 @@ type UsersController interface { All(c *fiber.Ctx) error Show(c *fiber.Ctx) error ShowByUsername(c *fiber.Ctx) error + CheckUsernameExists(c *fiber.Ctx) error ShowInfo(c *fiber.Ctx) error Save(c *fiber.Ctx) error Update(c *fiber.Ctx) error @@ -156,6 +156,36 @@ func (_i *usersController) ShowByUsername(c *fiber.Ctx) error { }) } +// CheckUsernameExists check if username exists +// @Summary Check if username exists +// @Description API for checking if username exists (returns only exist status) +// @Tags Users +// @Param username path string true "Username to check" +// @Success 200 {object} response.Response +// @Failure 400 {object} response.BadRequestError +// @Failure 500 {object} response.InternalServerError +// @Router /users/check-username/{username} [get] +func (_i *usersController) CheckUsernameExists(c *fiber.Ctx) error { + username := c.Params("username") + + exists, err := _i.usersService.CheckUsernameExists(username) + 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{"Username check completed"}, + Data: map[string]interface{}{ + "username": username, + "exists": exists, + }, + }) +} + // ShowInfo Users // @Summary ShowInfo Users // @Description API for ShowUserInfo diff --git a/app/module/users/service/users.service.go b/app/module/users/service/users.service.go index 2d88aa9..d0faa77 100644 --- a/app/module/users/service/users.service.go +++ b/app/module/users/service/users.service.go @@ -41,6 +41,7 @@ type UsersService interface { All(authToken string, req request.UsersQueryRequest) (users []*response.UsersResponse, paging paginator.Pagination, err error) Show(authToken string, id uint) (users *response.UsersResponse, err error) ShowByUsername(authToken string, username string) (users *response.UsersResponse, err error) + CheckUsernameExists(username string) (exists bool, err error) ShowUserInfo(authToken string) (users *response.UsersResponse, err error) Save(authToken string, req request.UsersCreateRequest) (userReturn *users.Users, err error) Login(req request.UserLogin) (res *gocloak.JWT, err error) @@ -142,6 +143,28 @@ func (_i *usersService) ShowByUsername(authToken string, username string) (users return mapper.UsersResponseMapper(result, _i.UserLevelsRepo, clientId), nil } +func (_i *usersService) CheckUsernameExists(username string) (exists bool, err error) { + _i.Log.Info().Str("username", username).Msg("Checking if username exists") + + // Check if username exists in repository + result, err := _i.Repo.FindByUsername(nil, username) + if err != nil { + // If error is "record not found", username doesn't exist + if strings.Contains(err.Error(), "record not found") { + return false, nil + } + _i.Log.Error().Err(err).Str("username", username).Msg("Failed to check username existence") + return false, err + } + + // If result is not nil, username exists + if result != nil { + return true, nil + } + + return false, nil +} + func (_i *usersService) ShowUserInfo(authToken string) (users *response.UsersResponse, err error) { // Extract clientId from authToken var clientId *uuid.UUID diff --git a/app/module/users/users.module.go b/app/module/users/users.module.go index 1f415f8..e63ffad 100644 --- a/app/module/users/users.module.go +++ b/app/module/users/users.module.go @@ -1,11 +1,12 @@ package users import ( - "github.com/gofiber/fiber/v2" - "go.uber.org/fx" "netidhub-saas-be/app/module/users/controller" "netidhub-saas-be/app/module/users/repository" "netidhub-saas-be/app/module/users/service" + + "github.com/gofiber/fiber/v2" + "go.uber.org/fx" ) // struct of UsersRouter @@ -47,6 +48,7 @@ func (_i *UsersRouter) RegisterUsersRoutes() { router.Get("/", usersController.All) router.Get("/detail/:id", usersController.Show) router.Get("/username/:username", usersController.ShowByUsername) + router.Get("/check-username/:username", usersController.CheckUsernameExists) router.Get("/info", usersController.ShowInfo) router.Post("/", usersController.Save) router.Put("/:id", usersController.Update) diff --git a/docs/CLIENT_NAME_CHECK_API.md b/docs/CLIENT_NAME_CHECK_API.md new file mode 100644 index 0000000..3e92514 --- /dev/null +++ b/docs/CLIENT_NAME_CHECK_API.md @@ -0,0 +1,277 @@ +# Client Name Check API Documentation + +## Overview +API endpoint untuk mengecek apakah nama client sudah ada atau belum dalam sistem. Endpoint ini mengembalikan status exist/not exist dengan pesan yang sesuai. + +## Endpoint + +### Check Client Name Exists +**GET** `/clients/check-name/{name}` + +#### Description +Mengecek apakah nama client sudah ada dalam sistem atau belum. + +#### Parameters +- **name** (path, required): Nama client yang akan dicek keberadaannya + +#### Headers +- Tidak memerlukan authentication (public endpoint) + +#### Response + +##### Success Response (200) - Name Available +```json +{ + "success": true, + "messages": ["Client name is available"], + "data": { + "name": "My Company", + "exists": false + } +} +``` + +##### Success Response (200) - Name Already Used +```json +{ + "success": true, + "messages": ["Name has been used"], + "data": { + "name": "My Company", + "exists": true + } +} +``` + +##### Response Fields +- **name** (string): Nama client yang dicek +- **exists** (boolean): Status apakah nama sudah ada (`true`) atau belum (`false`) + +#### Error Responses + +##### 500 Internal Server Error +```json +{ + "success": false, + "messages": ["Database error message"] +} +``` + +## Usage Examples + +### cURL Example +```bash +# Check if client name exists +curl -X GET "http://localhost:8080/clients/check-name/My%20Company" + +# Response if name exists +{ + "success": true, + "messages": ["Name has been used"], + "data": { + "name": "My Company", + "exists": true + } +} + +# Response if name doesn't exist +{ + "success": true, + "messages": ["Client name is available"], + "data": { + "name": "My Company", + "exists": false + } +} +``` + +### JavaScript Example +```javascript +const checkClientNameExists = async (name) => { + try { + const response = await fetch(`/clients/check-name/${encodeURIComponent(name)}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + + if (data.success) { + return { + exists: data.data.exists, + message: data.messages[0] + }; + } else { + console.error('Check failed:', data.messages); + return null; + } + } catch (error) { + console.error('Error checking client name:', error); + return null; + } +}; + +// Usage +const result = await checkClientNameExists('My Company'); +if (result) { + if (result.exists) { + console.log('Name is already used:', result.message); + } else { + console.log('Name is available:', result.message); + } +} +``` + +### React Hook Example +```javascript +import { useState, useEffect } from 'react'; + +const useClientNameCheck = (name) => { + const [exists, setExists] = useState(null); + const [message, setMessage] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const checkName = async () => { + if (!name || name.length < 2) { + setExists(null); + setMessage(''); + return; + } + + try { + setLoading(true); + setError(null); + + const response = await fetch(`/clients/check-name/${encodeURIComponent(name)}`); + const data = await response.json(); + + if (data.success) { + setExists(data.data.exists); + setMessage(data.messages[0]); + } else { + setError(data.messages[0]); + } + } catch (err) { + setError('Failed to check client name'); + } finally { + setLoading(false); + } + }; + + // Debounce the check + const timeoutId = setTimeout(checkName, 500); + return () => clearTimeout(timeoutId); + }, [name]); + + return { exists, message, loading, error }; +}; + +// Usage in component +const ClientNameInput = () => { + const [name, setName] = useState(''); + const { exists, message, loading, error } = useClientNameCheck(name); + + return ( +
+ setName(e.target.value)} + placeholder="Enter client name" + /> + + {loading && Checking...} + {error && Error: {error}} + {exists === true && {message}} + {exists === false && {message}} +
+ ); +}; +``` + +### Form Validation Example +```javascript +const validateClientName = async (name) => { + if (!name || name.length < 2) { + return { valid: false, message: 'Client name must be at least 2 characters' }; + } + + if (name.length > 100) { + return { valid: false, message: 'Client name must be less than 100 characters' }; + } + + try { + const response = await fetch(`/clients/check-name/${encodeURIComponent(name)}`); + const data = await response.json(); + + if (data.success) { + if (data.data.exists) { + return { valid: false, message: data.messages[0] }; + } else { + return { valid: true, message: data.messages[0] }; + } + } else { + return { valid: false, message: 'Unable to check client name availability' }; + } + } catch (error) { + return { valid: false, message: 'Network error occurred' }; + } +}; + +// Usage in form submission +const handleSubmit = async (formData) => { + const nameValidation = await validateClientName(formData.name); + if (!nameValidation.valid) { + setError(nameValidation.message); + return; + } + + // Proceed with form submission + submitForm(formData); +}; +``` + +## Use Cases +1. **Client Registration**: Mengecek ketersediaan nama client saat registrasi +2. **Real-time Validation**: Validasi nama client secara real-time saat user mengetik +3. **Client Name Suggestion**: Memberikan saran nama client alternatif jika nama sudah ada +4. **Profile Update**: Mengecek nama client baru saat user ingin mengubah nama client +5. **Admin Panel**: Admin mengecek ketersediaan nama client untuk client baru + +## Performance Considerations +- Endpoint ini tidak memerlukan authentication, sehingga lebih cepat +- Menggunakan query database yang efisien untuk pengecekan keberadaan +- Response yang minimal (hanya status exist/not exist) untuk performa optimal +- Cocok untuk real-time validation dengan debouncing + +## Security Notes +- Endpoint ini bersifat public dan tidak memerlukan authentication +- Tidak mengembalikan data sensitif client +- Hanya mengembalikan status boolean exist/not exist +- Nama client di-validate untuk mencegah injection attacks + +## Message Responses + +| Status | Message | Description | +|--------|---------|-------------| +| `exists: false` | "Client name is available" | Nama client tersedia untuk digunakan | +| `exists: true` | "Name has been used" | Nama client sudah digunakan | + +## Comparison with Other Endpoints + +| Endpoint | Purpose | Authentication | Response | +|----------|---------|----------------|----------| +| `/clients/check-name/{name}` | Check client name exists | Not required | Boolean status only | +| `/clients/{id}` | Get client by ID | Required | Full client data | +| `/clients/profile` | Get current client profile | Required | Current client data | + +## Notes +- Endpoint ini menggunakan method `FindByName` untuk pencarian berdasarkan nama +- Jika terjadi error database, akan mengembalikan error 500 +- Nama client case-sensitive (mengikuti konvensi sistem) +- Cocok untuk digunakan dalam form validation dan real-time checking +- URL encoding diperlukan untuk nama yang mengandung spasi atau karakter khusus diff --git a/docs/USERNAME_CHECK_API.md b/docs/USERNAME_CHECK_API.md new file mode 100644 index 0000000..b7a8089 --- /dev/null +++ b/docs/USERNAME_CHECK_API.md @@ -0,0 +1,251 @@ +# Username Check API Documentation + +## Overview +API endpoint untuk mengecek apakah username sudah ada atau belum dalam sistem. Endpoint ini hanya mengembalikan status exist/not exist tanpa mengembalikan data user lengkap. + +## Endpoint + +### Check Username Exists +**GET** `/users/check-username/{username}` + +#### Description +Mengecek apakah username sudah ada dalam sistem atau belum. + +#### Parameters +- **username** (path, required): Username yang akan dicek keberadaannya + +#### Headers +- Tidak memerlukan authentication (public endpoint) + +#### Response + +##### Success Response (200) +```json +{ + "success": true, + "messages": ["Username check completed"], + "data": { + "username": "john_doe", + "exists": true + } +} +``` + +##### Response Fields +- **username** (string): Username yang dicek +- **exists** (boolean): Status apakah username sudah ada (`true`) atau belum (`false`) + +#### Error Responses + +##### 500 Internal Server Error +```json +{ + "success": false, + "messages": ["Database error message"] +} +``` + +## Usage Examples + +### cURL Example +```bash +# Check if username exists +curl -X GET "http://localhost:8080/users/check-username/john_doe" + +# Response if username exists +{ + "success": true, + "messages": ["Username check completed"], + "data": { + "username": "john_doe", + "exists": true + } +} + +# Response if username doesn't exist +{ + "success": true, + "messages": ["Username check completed"], + "data": { + "username": "john_doe", + "exists": false + } +} +``` + +### JavaScript Example +```javascript +const checkUsernameExists = async (username) => { + try { + const response = await fetch(`/users/check-username/${username}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + const data = await response.json(); + + if (data.success) { + return data.data.exists; + } else { + console.error('Check failed:', data.messages); + return null; + } + } catch (error) { + console.error('Error checking username:', error); + return null; + } +}; + +// Usage +const usernameExists = await checkUsernameExists('john_doe'); +if (usernameExists === true) { + console.log('Username already exists'); +} else if (usernameExists === false) { + console.log('Username is available'); +} else { + console.log('Error occurred while checking'); +} +``` + +### React Hook Example +```javascript +import { useState, useEffect } from 'react'; + +const useUsernameCheck = (username) => { + const [exists, setExists] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const checkUsername = async () => { + if (!username || username.length < 3) { + setExists(null); + return; + } + + try { + setLoading(true); + setError(null); + + const response = await fetch(`/users/check-username/${username}`); + const data = await response.json(); + + if (data.success) { + setExists(data.data.exists); + } else { + setError(data.messages[0]); + } + } catch (err) { + setError('Failed to check username'); + } finally { + setLoading(false); + } + }; + + // Debounce the check + const timeoutId = setTimeout(checkUsername, 500); + return () => clearTimeout(timeoutId); + }, [username]); + + return { exists, loading, error }; +}; + +// Usage in component +const UsernameInput = () => { + const [username, setUsername] = useState(''); + const { exists, loading, error } = useUsernameCheck(username); + + return ( +
+ setUsername(e.target.value)} + placeholder="Enter username" + /> + + {loading && Checking...} + {error && Error: {error}} + {exists === true && Username already exists} + {exists === false && Username is available} +
+ ); +}; +``` + +### Form Validation Example +```javascript +const validateUsername = async (username) => { + if (!username || username.length < 3) { + return { valid: false, message: 'Username must be at least 3 characters' }; + } + + if (!/^[a-zA-Z0-9_]+$/.test(username)) { + return { valid: false, message: 'Username can only contain letters, numbers, and underscores' }; + } + + try { + const response = await fetch(`/users/check-username/${username}`); + const data = await response.json(); + + if (data.success) { + if (data.data.exists) { + return { valid: false, message: 'Username already exists' }; + } else { + return { valid: true, message: 'Username is available' }; + } + } else { + return { valid: false, message: 'Unable to check username availability' }; + } + } catch (error) { + return { valid: false, message: 'Network error occurred' }; + } +}; + +// Usage in form submission +const handleSubmit = async (formData) => { + const usernameValidation = await validateUsername(formData.username); + if (!usernameValidation.valid) { + setError(usernameValidation.message); + return; + } + + // Proceed with form submission + submitForm(formData); +}; +``` + +## Use Cases +1. **Registration Form**: Mengecek ketersediaan username saat user mendaftar +2. **Real-time Validation**: Validasi username secara real-time saat user mengetik +3. **Username Suggestion**: Memberikan saran username alternatif jika username sudah ada +4. **Profile Update**: Mengecek username baru saat user ingin mengubah username +5. **Admin Panel**: Admin mengecek ketersediaan username untuk user baru + +## Performance Considerations +- Endpoint ini tidak memerlukan authentication, sehingga lebih cepat +- Menggunakan query database yang efisien untuk pengecekan keberadaan +- Response yang minimal (hanya status exist/not exist) untuk performa optimal +- Cocok untuk real-time validation dengan debouncing + +## Security Notes +- Endpoint ini bersifat public dan tidak memerlukan authentication +- Tidak mengembalikan data sensitif user +- Hanya mengembalikan status boolean exist/not exist +- Username di-validate untuk mencegah injection attacks + +## Comparison with Other Endpoints + +| Endpoint | Purpose | Authentication | Response | +|----------|---------|----------------|----------| +| `/users/check-username/{username}` | Check username exists | Not required | Boolean status only | +| `/users/username/{username}` | Get user by username | Required | Full user data | +| `/users/info` | Get current user info | Required | Current user data | + +## Notes +- Endpoint ini menggunakan method `FindByUsername` dengan `clientId = nil` untuk pencarian global +- Jika terjadi error database, akan mengembalikan error 500 +- Username case-sensitive (mengikuti konvensi sistem) +- Cocok untuk digunakan dalam form validation dan real-time checking diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 4bba265..07c66a4 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -9744,6 +9744,44 @@ const docTemplate = `{ } } }, + "/clients/check-name/{name}": { + "get": { + "description": "API for checking if client name exists (returns only exist status)", + "tags": [ + "Clients" + ], + "summary": "Check if client name exists", + "parameters": [ + { + "type": "string", + "description": "Client name to check", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/clients/logo": { "post": { "security": [ @@ -9800,6 +9838,50 @@ const docTemplate = `{ } } }, + "/clients/logo/{filename}": { + "get": { + "description": "API for viewing client logo file by filename", + "tags": [ + "Clients" + ], + "summary": "View client logo", + "parameters": [ + { + "type": "string", + "description": "Logo filename", + "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" + } + } + } + } + }, "/clients/profile": { "get": { "security": [ @@ -15760,6 +15842,44 @@ const docTemplate = `{ } } }, + "/users/check-username/{username}": { + "get": { + "description": "API for checking if username exists (returns only exist status)", + "tags": [ + "Users" + ], + "summary": "Check if username exists", + "parameters": [ + { + "type": "string", + "description": "Username to check", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/users/detail/{id}": { "get": { "security": [ diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index a9c5193..38c0cb8 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -9733,6 +9733,44 @@ } } }, + "/clients/check-name/{name}": { + "get": { + "description": "API for checking if client name exists (returns only exist status)", + "tags": [ + "Clients" + ], + "summary": "Check if client name exists", + "parameters": [ + { + "type": "string", + "description": "Client name to check", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/clients/logo": { "post": { "security": [ @@ -9789,6 +9827,50 @@ } } }, + "/clients/logo/{filename}": { + "get": { + "description": "API for viewing client logo file by filename", + "tags": [ + "Clients" + ], + "summary": "View client logo", + "parameters": [ + { + "type": "string", + "description": "Logo filename", + "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" + } + } + } + } + }, "/clients/profile": { "get": { "security": [ @@ -15749,6 +15831,44 @@ } } }, + "/users/check-username/{username}": { + "get": { + "description": "API for checking if username exists (returns only exist status)", + "tags": [ + "Users" + ], + "summary": "Check if username exists", + "parameters": [ + { + "type": "string", + "description": "Username to check", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.BadRequestError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.InternalServerError" + } + } + } + } + }, "/users/detail/{id}": { "get": { "security": [ diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index ae7c2dd..97cce64 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -8240,6 +8240,31 @@ paths: summary: Bulk create sub-clients tags: - Clients + /clients/check-name/{name}: + get: + description: API for checking if client name exists (returns only exist status) + parameters: + - description: Client name to check + in: path + name: name + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + summary: Check if client name exists + tags: + - Clients /clients/logo: post: description: API for uploading client logo image to MinIO (uses client ID from @@ -8277,6 +8302,35 @@ paths: summary: Upload client logo tags: - Clients + /clients/logo/{filename}: + get: + description: API for viewing client logo file by filename + parameters: + - description: Logo filename + 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' + summary: View client logo + tags: + - Clients /clients/profile: get: description: API for getting Clients detail using client ID from auth token @@ -11817,6 +11871,31 @@ paths: summary: update Users tags: - Users + /users/check-username/{username}: + get: + description: API for checking if username exists (returns only exist status) + parameters: + - description: Username to check + in: path + name: username + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.BadRequestError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.InternalServerError' + summary: Check if username exists + tags: + - Users /users/detail/{id}: get: description: API for getting one Users diff --git a/netidhub-saas-be.exe b/netidhub-saas-be.exe new file mode 100644 index 0000000..88e8fc7 Binary files /dev/null and b/netidhub-saas-be.exe differ