fix: update client log

This commit is contained in:
hanif salafi 2025-10-12 17:15:10 +07:00
parent 95deebf106
commit dda6f6fea6
15 changed files with 1204 additions and 25 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter client name"
/>
{loading && <span>Checking...</span>}
{error && <span style={{color: 'red'}}>Error: {error}</span>}
{exists === true && <span style={{color: 'red'}}>{message}</span>}
{exists === false && <span style={{color: 'green'}}>{message}</span>}
</div>
);
};
```
### 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

251
docs/USERNAME_CHECK_API.md Normal file
View File

@ -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 (
<div>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter username"
/>
{loading && <span>Checking...</span>}
{error && <span style={{color: 'red'}}>Error: {error}</span>}
{exists === true && <span style={{color: 'red'}}>Username already exists</span>}
{exists === false && <span style={{color: 'green'}}>Username is available</span>}
</div>
);
};
```
### 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

View File

@ -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": [

View File

@ -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": [

View File

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

BIN
netidhub-saas-be.exe Normal file

Binary file not shown.