refact: modularization folder struct and fixing auth

This commit is contained in:
pahmiudahgede 2025-06-07 18:22:14 +07:00
parent 58f843cac2
commit e7c0675f8a
131 changed files with 9461 additions and 531 deletions

View File

@ -23,3 +23,8 @@ API_KEY=
#SECRET_KEY #SECRET_KEY
SECRET_KEY= SECRET_KEY=
# TTL
ACCESS_TOKEN_EXPIRY=
REFRESH_TOKEN_EXPIRY=
PARTIAL_TOKEN_EXPIRY=

View File

@ -1,6 +1,7 @@
package dto package dto
import ( import (
"rijig/utils"
"strings" "strings"
) )
@ -45,12 +46,8 @@ func (r *RequestCompanyProfileDTO) ValidateCompanyProfileInput() (map[string][]s
errors["company_Address"] = append(errors["company_address"], "Company address is required") errors["company_Address"] = append(errors["company_address"], "Company address is required")
} }
if strings.TrimSpace(r.CompanyPhone) == "" { if !utils.IsValidPhoneNumber(r.CompanyPhone) {
errors["company_Phone"] = append(errors["company_phone"], "Company phone is required") errors["company_Phone"] = append(errors["company_phone"], "nomor harus dimulai 62.. dan 8-14 digit")
}
if strings.TrimSpace(r.CompanyEmail) == "" {
errors["company_Email"] = append(errors["company_email"], "Company email is required")
} }
if strings.TrimSpace(r.CompanyDescription) == "" { if strings.TrimSpace(r.CompanyDescription) == "" {

View File

@ -1,5 +1,5 @@
package dto package dto
/*
import ( import (
"strings" "strings"
) )
@ -62,3 +62,4 @@ func (r *RequestTrashDetailDTO) ValidateTrashDetailInput() (map[string][]string,
return nil, true return nil, true
} }
*/

View File

@ -0,0 +1,66 @@
package about
import (
"strings"
)
type RequestAboutDTO struct {
Title string `json:"title"`
CoverImage string `json:"cover_image"`
}
func (r *RequestAboutDTO) ValidateAbout() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Title) == "" {
errors["title"] = append(errors["title"], "Title is required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
type ResponseAboutDTO struct {
ID string `json:"id"`
Title string `json:"title"`
CoverImage string `json:"cover_image"`
AboutDetail *[]ResponseAboutDetailDTO `json:"about_detail"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type RequestAboutDetailDTO struct {
AboutId string `json:"about_id"`
ImageDetail string `json:"image_detail"`
Description string `json:"description"`
}
func (r *RequestAboutDetailDTO) ValidateAboutDetail() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.AboutId) == "" {
errors["about_id"] = append(errors["about_id"], "about_id is required")
}
if strings.TrimSpace(r.Description) == "" {
errors["description"] = append(errors["description"], "Description is required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
type ResponseAboutDetailDTO struct {
ID string `json:"id"`
AboutID string `json:"about_id"`
ImageDetail string `json:"image_detail"`
Description string `json:"description"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}

View File

@ -0,0 +1,177 @@
package about
import (
"fmt"
"log"
"rijig/dto"
"rijig/internal/services"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
type AboutHandler struct {
AboutService services.AboutService
}
func NewAboutHandler(aboutService services.AboutService) *AboutHandler {
return &AboutHandler{
AboutService: aboutService,
}
}
func (h *AboutHandler) CreateAbout(c *fiber.Ctx) error {
var request dto.RequestAboutDTO
if err := c.BodyParser(&request); err != nil {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Invalid request body", map[string][]string{"body": {"Invalid body"}})
}
errors, valid := request.ValidateAbout()
if !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors)
}
aboutCoverImage, err := c.FormFile("cover_image")
if err != nil {
return utils.BadRequest(c, "Cover image is required")
}
response, err := h.AboutService.CreateAbout(request, aboutCoverImage)
if err != nil {
log.Printf("Error creating About: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to create About: %v", err))
}
return utils.CreateSuccessWithData(c, "Successfully created About", response)
}
func (h *AboutHandler) UpdateAbout(c *fiber.Ctx) error {
id := c.Params("id")
var request dto.RequestAboutDTO
if err := c.BodyParser(&request); err != nil {
log.Printf("Error parsing request body: %v", err)
return utils.BadRequest(c, "Invalid input data")
}
aboutCoverImage, err := c.FormFile("cover_image")
if err != nil {
log.Printf("Error retrieving cover image about from request: %v", err)
return utils.BadRequest(c, "cover_image is required")
}
response, err := h.AboutService.UpdateAbout(id, request, aboutCoverImage)
if err != nil {
log.Printf("Error updating About: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to update About: %v", err))
}
return utils.SuccessWithData(c, "Successfully updated About", response)
}
func (h *AboutHandler) GetAllAbout(c *fiber.Ctx) error {
response, err := h.AboutService.GetAllAbout()
if err != nil {
log.Printf("Error fetching all About: %v", err)
return utils.InternalServerError(c, "Failed to fetch About list")
}
return utils.SuccessWithPagination(c, "Successfully fetched About list", response, 1, len(response))
}
func (h *AboutHandler) GetAboutByID(c *fiber.Ctx) error {
id := c.Params("id")
response, err := h.AboutService.GetAboutByID(id)
if err != nil {
log.Printf("Error fetching About by ID: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to fetch About by ID: %v", err))
}
return utils.SuccessWithData(c, "Successfully fetched About", response)
}
func (h *AboutHandler) GetAboutDetailById(c *fiber.Ctx) error {
id := c.Params("id")
response, err := h.AboutService.GetAboutDetailById(id)
if err != nil {
log.Printf("Error fetching About detail by ID: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to fetch About by ID: %v", err))
}
return utils.SuccessWithData(c, "Successfully fetched About", response)
}
func (h *AboutHandler) DeleteAbout(c *fiber.Ctx) error {
id := c.Params("id")
if err := h.AboutService.DeleteAbout(id); err != nil {
log.Printf("Error deleting About: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to delete About: %v", err))
}
return utils.Success(c, "Successfully deleted About")
}
func (h *AboutHandler) CreateAboutDetail(c *fiber.Ctx) error {
var request dto.RequestAboutDetailDTO
if err := c.BodyParser(&request); err != nil {
log.Printf("Error parsing request body: %v", err)
return utils.BadRequest(c, "Invalid input data")
}
errors, valid := request.ValidateAboutDetail()
if !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors)
}
aboutDetailImage, err := c.FormFile("image_detail")
if err != nil {
log.Printf("Error retrieving image detail from request: %v", err)
return utils.BadRequest(c, "image_detail is required")
}
response, err := h.AboutService.CreateAboutDetail(request, aboutDetailImage)
if err != nil {
log.Printf("Error creating AboutDetail: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to create AboutDetail: %v", err))
}
return utils.CreateSuccessWithData(c, "Successfully created AboutDetail", response)
}
func (h *AboutHandler) UpdateAboutDetail(c *fiber.Ctx) error {
id := c.Params("id")
var request dto.RequestAboutDetailDTO
if err := c.BodyParser(&request); err != nil {
log.Printf("Error parsing request body: %v", err)
return utils.BadRequest(c, "Invalid input data")
}
aboutDetailImage, err := c.FormFile("image_detail")
if err != nil {
log.Printf("Error retrieving image detail from request: %v", err)
return utils.BadRequest(c, "image_detail is required")
}
response, err := h.AboutService.UpdateAboutDetail(id, request, aboutDetailImage)
if err != nil {
log.Printf("Error updating AboutDetail: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to update AboutDetail: %v", err))
}
return utils.SuccessWithData(c, "Successfully updated AboutDetail", response)
}
func (h *AboutHandler) DeleteAboutDetail(c *fiber.Ctx) error {
id := c.Params("id")
if err := h.AboutService.DeleteAboutDetail(id); err != nil {
log.Printf("Error deleting AboutDetail: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to delete AboutDetail: %v", err))
}
return utils.Success(c, "Successfully deleted AboutDetail")
}

View File

@ -0,0 +1,113 @@
package about
import (
"context"
"fmt"
"rijig/model"
"gorm.io/gorm"
)
type AboutRepository interface {
CreateAbout(ctx context.Context, about *model.About) error
CreateAboutDetail(ctx context.Context, aboutDetail *model.AboutDetail) error
GetAllAbout(ctx context.Context) ([]model.About, error)
GetAboutByID(ctx context.Context, id string) (*model.About, error)
GetAboutByIDWithoutPrel(ctx context.Context, id string) (*model.About, error)
GetAboutDetailByID(ctx context.Context, id string) (*model.AboutDetail, error)
UpdateAbout(ctx context.Context, id string, about *model.About) (*model.About, error)
UpdateAboutDetail(ctx context.Context, id string, aboutDetail *model.AboutDetail) (*model.AboutDetail, error)
DeleteAbout(ctx context.Context, id string) error
DeleteAboutDetail(ctx context.Context, id string) error
}
type aboutRepository struct {
db *gorm.DB
}
func NewAboutRepository(db *gorm.DB) AboutRepository {
return &aboutRepository{db}
}
func (r *aboutRepository) CreateAbout(ctx context.Context, about *model.About) error {
if err := r.db.WithContext(ctx).Create(&about).Error; err != nil {
return fmt.Errorf("failed to create About: %v", err)
}
return nil
}
func (r *aboutRepository) CreateAboutDetail(ctx context.Context, aboutDetail *model.AboutDetail) error {
if err := r.db.WithContext(ctx).Create(&aboutDetail).Error; err != nil {
return fmt.Errorf("failed to create AboutDetail: %v", err)
}
return nil
}
func (r *aboutRepository) GetAllAbout(ctx context.Context) ([]model.About, error) {
var abouts []model.About
if err := r.db.WithContext(ctx).Find(&abouts).Error; err != nil {
return nil, fmt.Errorf("failed to fetch all About records: %v", err)
}
return abouts, nil
}
func (r *aboutRepository) GetAboutByID(ctx context.Context, id string) (*model.About, error) {
var about model.About
if err := r.db.WithContext(ctx).Preload("AboutDetail").Where("id = ?", id).First(&about).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("about with ID %s not found", id)
}
return nil, fmt.Errorf("failed to fetch About by ID: %v", err)
}
return &about, nil
}
func (r *aboutRepository) GetAboutByIDWithoutPrel(ctx context.Context, id string) (*model.About, error) {
var about model.About
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&about).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("about with ID %s not found", id)
}
return nil, fmt.Errorf("failed to fetch About by ID: %v", err)
}
return &about, nil
}
func (r *aboutRepository) GetAboutDetailByID(ctx context.Context, id string) (*model.AboutDetail, error) {
var aboutDetail model.AboutDetail
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&aboutDetail).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("aboutdetail with ID %s not found", id)
}
return nil, fmt.Errorf("failed to fetch AboutDetail by ID: %v", err)
}
return &aboutDetail, nil
}
func (r *aboutRepository) UpdateAbout(ctx context.Context, id string, about *model.About) (*model.About, error) {
if err := r.db.WithContext(ctx).Model(&about).Where("id = ?", id).Updates(about).Error; err != nil {
return nil, fmt.Errorf("failed to update About: %v", err)
}
return about, nil
}
func (r *aboutRepository) UpdateAboutDetail(ctx context.Context, id string, aboutDetail *model.AboutDetail) (*model.AboutDetail, error) {
if err := r.db.WithContext(ctx).Model(&aboutDetail).Where("id = ?", id).Updates(aboutDetail).Error; err != nil {
return nil, fmt.Errorf("failed to update AboutDetail: %v", err)
}
return aboutDetail, nil
}
func (r *aboutRepository) DeleteAbout(ctx context.Context, id string) error {
if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.About{}).Error; err != nil {
return fmt.Errorf("failed to delete About: %v", err)
}
return nil
}
func (r *aboutRepository) DeleteAboutDetail(ctx context.Context, id string) error {
if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.AboutDetail{}).Error; err != nil {
return fmt.Errorf("failed to delete AboutDetail: %v", err)
}
return nil
}

View File

@ -0,0 +1 @@
package about

View File

@ -0,0 +1,497 @@
package about
import (
"context"
"fmt"
"log"
"mime/multipart"
"os"
"path/filepath"
"rijig/dto"
"rijig/model"
"rijig/utils"
"time"
"github.com/google/uuid"
)
const (
cacheKeyAllAbout = "about:all"
cacheKeyAboutByID = "about:id:%s"
cacheKeyAboutDetail = "about_detail:id:%s"
cacheTTL = 30 * time.Minute
)
type AboutService interface {
CreateAbout(ctx context.Context, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error)
UpdateAbout(ctx context.Context, id string, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error)
GetAllAbout(ctx context.Context) ([]dto.ResponseAboutDTO, error)
GetAboutByID(ctx context.Context, id string) (*dto.ResponseAboutDTO, error)
GetAboutDetailById(ctx context.Context, id string) (*dto.ResponseAboutDetailDTO, error)
DeleteAbout(ctx context.Context, id string) error
CreateAboutDetail(ctx context.Context, request dto.RequestAboutDetailDTO, coverImageAboutDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error)
UpdateAboutDetail(ctx context.Context, id string, request dto.RequestAboutDetailDTO, imageDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error)
DeleteAboutDetail(ctx context.Context, id string) error
}
type aboutService struct {
aboutRepo AboutRepository
}
func NewAboutService(aboutRepo AboutRepository) AboutService {
return &aboutService{aboutRepo: aboutRepo}
}
func (s *aboutService) invalidateAboutCaches(aboutID string) {
if err := utils.DeleteCache(cacheKeyAllAbout); err != nil {
log.Printf("Failed to invalidate all about cache: %v", err)
}
aboutCacheKey := fmt.Sprintf(cacheKeyAboutByID, aboutID)
if err := utils.DeleteCache(aboutCacheKey); err != nil {
log.Printf("Failed to invalidate about cache for ID %s: %v", aboutID, err)
}
}
func (s *aboutService) invalidateAboutDetailCaches(aboutDetailID, aboutID string) {
detailCacheKey := fmt.Sprintf(cacheKeyAboutDetail, aboutDetailID)
if err := utils.DeleteCache(detailCacheKey); err != nil {
log.Printf("Failed to invalidate about detail cache for ID %s: %v", aboutDetailID, err)
}
s.invalidateAboutCaches(aboutID)
}
func formatResponseAboutDetailDTO(about *model.AboutDetail) (*dto.ResponseAboutDetailDTO, error) {
createdAt, _ := utils.FormatDateToIndonesianFormat(about.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(about.UpdatedAt)
response := &dto.ResponseAboutDetailDTO{
ID: about.ID,
AboutID: about.AboutID,
ImageDetail: about.ImageDetail,
Description: about.Description,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
return response, nil
}
func formatResponseAboutDTO(about *model.About) (*dto.ResponseAboutDTO, error) {
createdAt, _ := utils.FormatDateToIndonesianFormat(about.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(about.UpdatedAt)
response := &dto.ResponseAboutDTO{
ID: about.ID,
Title: about.Title,
CoverImage: about.CoverImage,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
return response, nil
}
func (s *aboutService) saveCoverImageAbout(coverImageAbout *multipart.FileHeader) (string, error) {
pathImage := "/uploads/coverabout/"
coverImageAboutDir := "./public" + os.Getenv("BASE_URL") + pathImage
if _, err := os.Stat(coverImageAboutDir); os.IsNotExist(err) {
if err := os.MkdirAll(coverImageAboutDir, os.ModePerm); err != nil {
return "", fmt.Errorf("gagal membuat direktori untuk cover image about: %v", err)
}
}
allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true}
extension := filepath.Ext(coverImageAbout.Filename)
if !allowedExtensions[extension] {
return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, .png, and .svg are allowed")
}
coverImageFileName := fmt.Sprintf("%s_coverabout%s", uuid.New().String(), extension)
coverImagePath := filepath.Join(coverImageAboutDir, coverImageFileName)
src, err := coverImageAbout.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %v", err)
}
defer src.Close()
dst, err := os.Create(coverImagePath)
if err != nil {
return "", fmt.Errorf("failed to create cover image about file: %v", err)
}
defer dst.Close()
if _, err := dst.ReadFrom(src); err != nil {
return "", fmt.Errorf("failed to save cover image about: %v", err)
}
coverImageAboutUrl := fmt.Sprintf("%s%s", pathImage, coverImageFileName)
return coverImageAboutUrl, nil
}
func (s *aboutService) saveCoverImageAboutDetail(coverImageAbout *multipart.FileHeader) (string, error) {
pathImage := "/uploads/coverabout/coveraboutdetail/"
coverImageAboutDir := "./public" + os.Getenv("BASE_URL") + pathImage
if _, err := os.Stat(coverImageAboutDir); os.IsNotExist(err) {
if err := os.MkdirAll(coverImageAboutDir, os.ModePerm); err != nil {
return "", fmt.Errorf("gagal membuat direktori untuk cover image about detail: %v", err)
}
}
allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true}
extension := filepath.Ext(coverImageAbout.Filename)
if !allowedExtensions[extension] {
return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, .png, and .svg are allowed")
}
coverImageFileName := fmt.Sprintf("%s_coveraboutdetail%s", uuid.New().String(), extension)
coverImagePath := filepath.Join(coverImageAboutDir, coverImageFileName)
src, err := coverImageAbout.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %v", err)
}
defer src.Close()
dst, err := os.Create(coverImagePath)
if err != nil {
return "", fmt.Errorf("failed to create cover image about detail file: %v", err)
}
defer dst.Close()
if _, err := dst.ReadFrom(src); err != nil {
return "", fmt.Errorf("failed to save cover image about detail: %v", err)
}
coverImageAboutUrl := fmt.Sprintf("%s%s", pathImage, coverImageFileName)
return coverImageAboutUrl, nil
}
func deleteCoverImageAbout(coverimageAboutPath string) error {
if coverimageAboutPath == "" {
return nil
}
baseDir := "./public/" + os.Getenv("BASE_URL")
absolutePath := baseDir + coverimageAboutPath
if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
return fmt.Errorf("image file not found: %v", err)
}
err := os.Remove(absolutePath)
if err != nil {
return fmt.Errorf("failed to delete image: %v", err)
}
log.Printf("Image deleted successfully: %s", absolutePath)
return nil
}
func (s *aboutService) CreateAbout(ctx context.Context, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) {
errors, valid := request.ValidateAbout()
if !valid {
return nil, fmt.Errorf("validation error: %v", errors)
}
coverImageAboutPath, err := s.saveCoverImageAbout(coverImageAbout)
if err != nil {
return nil, fmt.Errorf("gagal menyimpan cover image about: %v", err)
}
about := model.About{
Title: request.Title,
CoverImage: coverImageAboutPath,
}
if err := s.aboutRepo.CreateAbout(ctx, &about); err != nil {
return nil, fmt.Errorf("failed to create About: %v", err)
}
response, err := formatResponseAboutDTO(&about)
if err != nil {
return nil, fmt.Errorf("error formatting About response: %v", err)
}
s.invalidateAboutCaches("")
return response, nil
}
func (s *aboutService) UpdateAbout(ctx context.Context, id string, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) {
errors, valid := request.ValidateAbout()
if !valid {
return nil, fmt.Errorf("validation error: %v", errors)
}
about, err := s.aboutRepo.GetAboutByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("about not found: %v", err)
}
oldCoverImage := about.CoverImage
var coverImageAboutPath string
if coverImageAbout != nil {
coverImageAboutPath, err = s.saveCoverImageAbout(coverImageAbout)
if err != nil {
return nil, fmt.Errorf("gagal menyimpan gambar baru: %v", err)
}
}
about.Title = request.Title
if coverImageAboutPath != "" {
about.CoverImage = coverImageAboutPath
}
updatedAbout, err := s.aboutRepo.UpdateAbout(ctx, id, about)
if err != nil {
return nil, fmt.Errorf("failed to update About: %v", err)
}
if oldCoverImage != "" && coverImageAboutPath != "" {
if err := deleteCoverImageAbout(oldCoverImage); err != nil {
log.Printf("Warning: failed to delete old image: %v", err)
}
}
response, err := formatResponseAboutDTO(updatedAbout)
if err != nil {
return nil, fmt.Errorf("error formatting About response: %v", err)
}
s.invalidateAboutCaches(id)
return response, nil
}
func (s *aboutService) GetAllAbout(ctx context.Context) ([]dto.ResponseAboutDTO, error) {
var cachedAbouts []dto.ResponseAboutDTO
if err := utils.GetCache(cacheKeyAllAbout, &cachedAbouts); err == nil {
return cachedAbouts, nil
}
aboutList, err := s.aboutRepo.GetAllAbout(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get About list: %v", err)
}
var aboutDTOList []dto.ResponseAboutDTO
for _, about := range aboutList {
response, err := formatResponseAboutDTO(&about)
if err != nil {
log.Printf("Error formatting About response: %v", err)
continue
}
aboutDTOList = append(aboutDTOList, *response)
}
if err := utils.SetCache(cacheKeyAllAbout, aboutDTOList, cacheTTL); err != nil {
log.Printf("Failed to cache all about data: %v", err)
}
return aboutDTOList, nil
}
func (s *aboutService) GetAboutByID(ctx context.Context, id string) (*dto.ResponseAboutDTO, error) {
cacheKey := fmt.Sprintf(cacheKeyAboutByID, id)
var cachedAbout dto.ResponseAboutDTO
if err := utils.GetCache(cacheKey, &cachedAbout); err == nil {
return &cachedAbout, nil
}
about, err := s.aboutRepo.GetAboutByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("about not found: %v", err)
}
response, err := formatResponseAboutDTO(about)
if err != nil {
return nil, fmt.Errorf("error formatting About response: %v", err)
}
var responseDetails []dto.ResponseAboutDetailDTO
for _, detail := range about.AboutDetail {
formattedDetail, err := formatResponseAboutDetailDTO(&detail)
if err != nil {
return nil, fmt.Errorf("error formatting AboutDetail response: %v", err)
}
responseDetails = append(responseDetails, *formattedDetail)
}
response.AboutDetail = &responseDetails
if err := utils.SetCache(cacheKey, response, cacheTTL); err != nil {
log.Printf("Failed to cache about data for ID %s: %v", id, err)
}
return response, nil
}
func (s *aboutService) GetAboutDetailById(ctx context.Context, id string) (*dto.ResponseAboutDetailDTO, error) {
cacheKey := fmt.Sprintf(cacheKeyAboutDetail, id)
var cachedDetail dto.ResponseAboutDetailDTO
if err := utils.GetCache(cacheKey, &cachedDetail); err == nil {
return &cachedDetail, nil
}
aboutDetail, err := s.aboutRepo.GetAboutDetailByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("about detail not found: %v", err)
}
response, err := formatResponseAboutDetailDTO(aboutDetail)
if err != nil {
return nil, fmt.Errorf("error formatting AboutDetail response: %v", err)
}
if err := utils.SetCache(cacheKey, response, cacheTTL); err != nil {
log.Printf("Failed to cache about detail data for ID %s: %v", id, err)
}
return response, nil
}
func (s *aboutService) DeleteAbout(ctx context.Context, id string) error {
about, err := s.aboutRepo.GetAboutByID(ctx, id)
if err != nil {
return fmt.Errorf("about not found: %v", err)
}
if about.CoverImage != "" {
if err := deleteCoverImageAbout(about.CoverImage); err != nil {
log.Printf("Warning: failed to delete cover image: %v", err)
}
}
for _, detail := range about.AboutDetail {
if detail.ImageDetail != "" {
if err := deleteCoverImageAbout(detail.ImageDetail); err != nil {
log.Printf("Warning: failed to delete detail image: %v", err)
}
}
}
if err := s.aboutRepo.DeleteAbout(ctx, id); err != nil {
return fmt.Errorf("failed to delete About: %v", err)
}
s.invalidateAboutCaches(id)
return nil
}
func (s *aboutService) CreateAboutDetail(ctx context.Context, request dto.RequestAboutDetailDTO, coverImageAboutDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) {
errors, valid := request.ValidateAboutDetail()
if !valid {
return nil, fmt.Errorf("validation error: %v", errors)
}
_, err := s.aboutRepo.GetAboutByIDWithoutPrel(ctx, request.AboutId)
if err != nil {
return nil, fmt.Errorf("about_id tidak ditemukan: %v", err)
}
coverImageAboutDetailPath, err := s.saveCoverImageAboutDetail(coverImageAboutDetail)
if err != nil {
return nil, fmt.Errorf("gagal menyimpan cover image about detail: %v", err)
}
aboutDetail := model.AboutDetail{
AboutID: request.AboutId,
ImageDetail: coverImageAboutDetailPath,
Description: request.Description,
}
if err := s.aboutRepo.CreateAboutDetail(ctx, &aboutDetail); err != nil {
return nil, fmt.Errorf("failed to create AboutDetail: %v", err)
}
response, err := formatResponseAboutDetailDTO(&aboutDetail)
if err != nil {
return nil, fmt.Errorf("error formatting AboutDetail response: %v", err)
}
s.invalidateAboutDetailCaches("", request.AboutId)
return response, nil
}
func (s *aboutService) UpdateAboutDetail(ctx context.Context, id string, request dto.RequestAboutDetailDTO, imageDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) {
errors, valid := request.ValidateAboutDetail()
if !valid {
return nil, fmt.Errorf("validation error: %v", errors)
}
aboutDetail, err := s.aboutRepo.GetAboutDetailByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("about detail tidak ditemukan: %v", err)
}
oldImageDetail := aboutDetail.ImageDetail
var coverImageAboutDetailPath string
if imageDetail != nil {
coverImageAboutDetailPath, err = s.saveCoverImageAboutDetail(imageDetail)
if err != nil {
return nil, fmt.Errorf("gagal menyimpan gambar baru: %v", err)
}
}
aboutDetail.Description = request.Description
if coverImageAboutDetailPath != "" {
aboutDetail.ImageDetail = coverImageAboutDetailPath
}
updatedAboutDetail, err := s.aboutRepo.UpdateAboutDetail(ctx, id, aboutDetail)
if err != nil {
return nil, fmt.Errorf("failed to update AboutDetail: %v", err)
}
if oldImageDetail != "" && coverImageAboutDetailPath != "" {
if err := deleteCoverImageAbout(oldImageDetail); err != nil {
log.Printf("Warning: failed to delete old detail image: %v", err)
}
}
response, err := formatResponseAboutDetailDTO(updatedAboutDetail)
if err != nil {
return nil, fmt.Errorf("error formatting AboutDetail response: %v", err)
}
s.invalidateAboutDetailCaches(id, aboutDetail.AboutID)
return response, nil
}
func (s *aboutService) DeleteAboutDetail(ctx context.Context, id string) error {
aboutDetail, err := s.aboutRepo.GetAboutDetailByID(ctx, id)
if err != nil {
return fmt.Errorf("about detail tidak ditemukan: %v", err)
}
aboutID := aboutDetail.AboutID
if aboutDetail.ImageDetail != "" {
if err := deleteCoverImageAbout(aboutDetail.ImageDetail); err != nil {
log.Printf("Warning: failed to delete detail image: %v", err)
}
}
if err := s.aboutRepo.DeleteAboutDetail(ctx, id); err != nil {
return fmt.Errorf("failed to delete AboutDetail: %v", err)
}
s.invalidateAboutDetailCaches(id, aboutID)
return nil
}

View File

@ -0,0 +1,73 @@
package address
import "strings"
type AddressResponseDTO struct {
UserID string `json:"user_id,omitempty"`
ID string `json:"address_id,omitempty"`
Province string `json:"province,omitempty"`
Regency string `json:"regency,omitempty"`
District string `json:"district,omitempty"`
Village string `json:"village,omitempty"`
PostalCode string `json:"postalCode,omitempty"`
Detail string `json:"detail,omitempty"`
Latitude float64 `json:"latitude,omitempty"`
Longitude float64 `json:"longitude,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
}
type CreateAddressDTO struct {
Province string `json:"province_id"`
Regency string `json:"regency_id"`
District string `json:"district_id"`
Village string `json:"village_id"`
PostalCode string `json:"postalCode"`
Detail string `json:"detail"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
func (r *CreateAddressDTO) ValidateAddress() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Province) == "" {
errors["province_id"] = append(errors["province_id"], "Province ID is required")
}
if strings.TrimSpace(r.Regency) == "" {
errors["regency_id"] = append(errors["regency_id"], "Regency ID is required")
}
if strings.TrimSpace(r.District) == "" {
errors["district_id"] = append(errors["district_id"], "District ID is required")
}
if strings.TrimSpace(r.Village) == "" {
errors["village_id"] = append(errors["village_id"], "Village ID is required")
}
if strings.TrimSpace(r.PostalCode) == "" {
errors["postalCode"] = append(errors["postalCode"], "PostalCode is required")
} else if len(r.PostalCode) < 5 {
errors["postalCode"] = append(errors["postalCode"], "PostalCode must be at least 5 characters")
}
if strings.TrimSpace(r.Detail) == "" {
errors["detail"] = append(errors["detail"], "Detail address is required")
}
if r.Latitude == 0 {
errors["latitude"] = append(errors["latitude"], "Latitude is required")
}
if r.Longitude == 0 {
errors["longitude"] = append(errors["longitude"], "Longitude is required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1 @@
package address

View File

@ -0,0 +1,62 @@
package address
import (
"context"
"rijig/model"
"gorm.io/gorm"
)
type AddressRepository interface {
CreateAddress(ctx context.Context, address *model.Address) error
FindAddressByUserID(ctx context.Context, userID string) ([]model.Address, error)
FindAddressByID(ctx context.Context, id string) (*model.Address, error)
UpdateAddress(ctx context.Context, address *model.Address) error
DeleteAddress(ctx context.Context, id string) error
}
type addressRepository struct {
db *gorm.DB
}
func NewAddressRepository(db *gorm.DB) AddressRepository {
return &addressRepository{db}
}
func (r *addressRepository) CreateAddress(ctx context.Context, address *model.Address) error {
return r.db.WithContext(ctx).Create(address).Error
}
func (r *addressRepository) FindAddressByUserID(ctx context.Context, userID string) ([]model.Address, error) {
var addresses []model.Address
err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&addresses).Error
if err != nil {
return nil, err
}
return addresses, nil
}
func (r *addressRepository) FindAddressByID(ctx context.Context, id string) (*model.Address, error) {
var address model.Address
err := r.db.WithContext(ctx).Where("id = ?", id).First(&address).Error
if err != nil {
return nil, err
}
return &address, nil
}
func (r *addressRepository) UpdateAddress(ctx context.Context, address *model.Address) error {
err := r.db.WithContext(ctx).Save(address).Error
if err != nil {
return err
}
return nil
}
func (r *addressRepository) DeleteAddress(ctx context.Context, id string) error {
err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.Address{}).Error
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1 @@
package address

View File

@ -0,0 +1,250 @@
package address
import (
"context"
"errors"
"fmt"
"time"
"rijig/dto"
"rijig/internal/wilayahindo"
"rijig/model"
"rijig/utils"
)
const (
cacheTTL = time.Hour * 24
userAddressesCacheKeyPattern = "user:%s:addresses"
addressCacheKeyPattern = "address:%s"
)
type AddressService interface {
CreateAddress(ctx context.Context, userID string, request dto.CreateAddressDTO) (*dto.AddressResponseDTO, error)
GetAddressByUserID(ctx context.Context, userID string) ([]dto.AddressResponseDTO, error)
GetAddressByID(ctx context.Context, userID, id string) (*dto.AddressResponseDTO, error)
UpdateAddress(ctx context.Context, userID, id string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error)
DeleteAddress(ctx context.Context, userID, id string) error
}
type addressService struct {
addressRepo AddressRepository
wilayahRepo wilayahindo.WilayahIndonesiaRepository
}
func NewAddressService(addressRepo AddressRepository, wilayahRepo wilayahindo.WilayahIndonesiaRepository) AddressService {
return &addressService{
addressRepo: addressRepo,
wilayahRepo: wilayahRepo,
}
}
func (s *addressService) validateWilayahIDs(ctx context.Context, addressDTO dto.CreateAddressDTO) (string, string, string, string, error) {
province, _, err := s.wilayahRepo.FindProvinceByID(ctx, addressDTO.Province, 0, 0)
if err != nil {
return "", "", "", "", fmt.Errorf("invalid province_id: %w", err)
}
regency, _, err := s.wilayahRepo.FindRegencyByID(ctx, addressDTO.Regency, 0, 0)
if err != nil {
return "", "", "", "", fmt.Errorf("invalid regency_id: %w", err)
}
district, _, err := s.wilayahRepo.FindDistrictByID(ctx, addressDTO.District, 0, 0)
if err != nil {
return "", "", "", "", fmt.Errorf("invalid district_id: %w", err)
}
village, err := s.wilayahRepo.FindVillageByID(ctx, addressDTO.Village)
if err != nil {
return "", "", "", "", fmt.Errorf("invalid village_id: %w", err)
}
return province.Name, regency.Name, district.Name, village.Name, nil
}
func (s *addressService) mapToResponseDTO(address *model.Address) *dto.AddressResponseDTO {
createdAt, _ := utils.FormatDateToIndonesianFormat(address.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(address.UpdatedAt)
return &dto.AddressResponseDTO{
UserID: address.UserID,
ID: address.ID,
Province: address.Province,
Regency: address.Regency,
District: address.District,
Village: address.Village,
PostalCode: address.PostalCode,
Detail: address.Detail,
Latitude: address.Latitude,
Longitude: address.Longitude,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
}
func (s *addressService) invalidateAddressCaches(userID, addressID string) {
if addressID != "" {
addressCacheKey := fmt.Sprintf(addressCacheKeyPattern, addressID)
if err := utils.DeleteCache(addressCacheKey); err != nil {
fmt.Printf("Error deleting address cache: %v\n", err)
}
}
userCacheKey := fmt.Sprintf(userAddressesCacheKeyPattern, userID)
if err := utils.DeleteCache(userCacheKey); err != nil {
fmt.Printf("Error deleting user addresses cache: %v\n", err)
}
}
func (s *addressService) cacheAddress(addressDTO *dto.AddressResponseDTO) {
cacheKey := fmt.Sprintf(addressCacheKeyPattern, addressDTO.ID)
if err := utils.SetCache(cacheKey, addressDTO, cacheTTL); err != nil {
fmt.Printf("Error caching address to Redis: %v\n", err)
}
}
func (s *addressService) cacheUserAddresses(ctx context.Context, userID string) ([]dto.AddressResponseDTO, error) {
addresses, err := s.addressRepo.FindAddressByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to fetch addresses: %w", err)
}
var addressDTOs []dto.AddressResponseDTO
for _, address := range addresses {
addressDTOs = append(addressDTOs, *s.mapToResponseDTO(&address))
}
cacheKey := fmt.Sprintf(userAddressesCacheKeyPattern, userID)
if err := utils.SetCache(cacheKey, addressDTOs, cacheTTL); err != nil {
fmt.Printf("Error caching addresses to Redis: %v\n", err)
}
return addressDTOs, nil
}
func (s *addressService) checkAddressOwnership(ctx context.Context, userID, addressID string) (*model.Address, error) {
address, err := s.addressRepo.FindAddressByID(ctx, addressID)
if err != nil {
return nil, fmt.Errorf("address not found: %w", err)
}
if address.UserID != userID {
return nil, errors.New("you are not authorized to access this address")
}
return address, nil
}
func (s *addressService) CreateAddress(ctx context.Context, userID string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) {
provinceName, regencyName, districtName, villageName, err := s.validateWilayahIDs(ctx, addressDTO)
if err != nil {
return nil, err
}
address := model.Address{
UserID: userID,
Province: provinceName,
Regency: regencyName,
District: districtName,
Village: villageName,
PostalCode: addressDTO.PostalCode,
Detail: addressDTO.Detail,
Latitude: addressDTO.Latitude,
Longitude: addressDTO.Longitude,
}
if err := s.addressRepo.CreateAddress(ctx, &address); err != nil {
return nil, fmt.Errorf("failed to create address: %w", err)
}
responseDTO := s.mapToResponseDTO(&address)
s.cacheAddress(responseDTO)
s.invalidateAddressCaches(userID, "")
return responseDTO, nil
}
func (s *addressService) GetAddressByUserID(ctx context.Context, userID string) ([]dto.AddressResponseDTO, error) {
cacheKey := fmt.Sprintf(userAddressesCacheKeyPattern, userID)
var cachedAddresses []dto.AddressResponseDTO
if err := utils.GetCache(cacheKey, &cachedAddresses); err == nil {
return cachedAddresses, nil
}
return s.cacheUserAddresses(ctx, userID)
}
func (s *addressService) GetAddressByID(ctx context.Context, userID, id string) (*dto.AddressResponseDTO, error) {
address, err := s.checkAddressOwnership(ctx, userID, id)
if err != nil {
return nil, err
}
cacheKey := fmt.Sprintf(addressCacheKeyPattern, id)
var cachedAddress dto.AddressResponseDTO
if err := utils.GetCache(cacheKey, &cachedAddress); err == nil {
return &cachedAddress, nil
}
responseDTO := s.mapToResponseDTO(address)
s.cacheAddress(responseDTO)
return responseDTO, nil
}
func (s *addressService) UpdateAddress(ctx context.Context, userID, id string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) {
address, err := s.checkAddressOwnership(ctx, userID, id)
if err != nil {
return nil, err
}
provinceName, regencyName, districtName, villageName, err := s.validateWilayahIDs(ctx, addressDTO)
if err != nil {
return nil, err
}
address.Province = provinceName
address.Regency = regencyName
address.District = districtName
address.Village = villageName
address.PostalCode = addressDTO.PostalCode
address.Detail = addressDTO.Detail
address.Latitude = addressDTO.Latitude
address.Longitude = addressDTO.Longitude
if err := s.addressRepo.UpdateAddress(ctx, address); err != nil {
return nil, fmt.Errorf("failed to update address: %w", err)
}
responseDTO := s.mapToResponseDTO(address)
s.cacheAddress(responseDTO)
s.invalidateAddressCaches(userID, "")
return responseDTO, nil
}
func (s *addressService) DeleteAddress(ctx context.Context, userID, addressID string) error {
address, err := s.checkAddressOwnership(ctx, userID, addressID)
if err != nil {
return err
}
if err := s.addressRepo.DeleteAddress(ctx, addressID); err != nil {
return fmt.Errorf("failed to delete address: %w", err)
}
s.invalidateAddressCaches(address.UserID, addressID)
return nil
}

View File

@ -0,0 +1,48 @@
package article
import (
"strings"
)
type ArticleResponseDTO struct {
ID string `json:"article_id"`
Title string `json:"title"`
CoverImage string `json:"coverImage"`
Author string `json:"author"`
Heading string `json:"heading"`
Content string `json:"content"`
PublishedAt string `json:"publishedAt"`
UpdatedAt string `json:"updatedAt"`
}
type RequestArticleDTO struct {
Title string `json:"title"`
CoverImage string `json:"coverImage"`
Author string `json:"author"`
Heading string `json:"heading"`
Content string `json:"content"`
}
func (r *RequestArticleDTO) ValidateRequestArticleDTO() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Title) == "" {
errors["title"] = append(errors["title"], "Title is required")
}
if strings.TrimSpace(r.Author) == "" {
errors["author"] = append(errors["author"], "Author is required")
}
if strings.TrimSpace(r.Heading) == "" {
errors["heading"] = append(errors["heading"], "Heading is required")
}
if strings.TrimSpace(r.Content) == "" {
errors["content"] = append(errors["content"], "Content is required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1,141 @@
package article
import (
"mime/multipart"
"rijig/utils"
"strconv"
"github.com/gofiber/fiber/v2"
)
type ArticleHandler struct {
articleService ArticleService
}
func NewArticleHandler(articleService ArticleService) *ArticleHandler {
return &ArticleHandler{articleService}
}
func (h *ArticleHandler) CreateArticle(c *fiber.Ctx) error {
var request RequestArticleDTO
if err := c.BodyParser(&request); err != nil {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Invalid request body", map[string][]string{"body": {"Invalid body"}})
}
errors, valid := request.ValidateRequestArticleDTO()
if !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors)
}
coverImage, err := c.FormFile("coverImage")
if err != nil {
return utils.BadRequest(c, "Cover image is required")
}
articleResponse, err := h.articleService.CreateArticle(c.Context(), request, coverImage)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.SuccessWithData(c, "Article created successfully", articleResponse)
}
func (h *ArticleHandler) GetAllArticles(c *fiber.Ctx) error {
page, err := strconv.Atoi(c.Query("page", "0"))
if err != nil || page < 0 {
page = 0
}
limit, err := strconv.Atoi(c.Query("limit", "0"))
if err != nil || limit < 0 {
limit = 0
}
articles, totalArticles, err := h.articleService.GetAllArticles(c.Context(), page, limit)
if err != nil {
return utils.InternalServerError(c, "Failed to fetch articles")
}
responseData := map[string]interface{}{
"articles": articles,
"total": int(totalArticles),
}
if page == 0 && limit == 0 {
return utils.SuccessWithData(c, "Articles fetched successfully", responseData)
}
return utils.SuccessWithPagination(c, "Articles fetched successfully", responseData, page, limit)
}
func (h *ArticleHandler) GetArticleByID(c *fiber.Ctx) error {
id := c.Params("article_id")
if id == "" {
return utils.BadRequest(c, "Article ID is required")
}
article, err := h.articleService.GetArticleByID(c.Context(), id)
if err != nil {
return utils.NotFound(c, "Article not found")
}
return utils.SuccessWithData(c, "Article fetched successfully", article)
}
func (h *ArticleHandler) UpdateArticle(c *fiber.Ctx) error {
id := c.Params("article_id")
if id == "" {
return utils.BadRequest(c, "Article ID is required")
}
var request RequestArticleDTO
if err := c.BodyParser(&request); err != nil {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Invalid request body", map[string][]string{"body": {"Invalid body"}})
}
errors, valid := request.ValidateRequestArticleDTO()
if !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors)
}
var coverImage *multipart.FileHeader
coverImage, err := c.FormFile("coverImage")
if err != nil && err.Error() != "no such file" && err.Error() != "there is no uploaded file associated with the given key" {
return utils.BadRequest(c, "Invalid cover image")
}
articleResponse, err := h.articleService.UpdateArticle(c.Context(), id, request, coverImage)
if err != nil {
if isNotFoundError(err) {
return utils.NotFound(c, err.Error())
}
return utils.InternalServerError(c, err.Error())
}
return utils.SuccessWithData(c, "Article updated successfully", articleResponse)
}
func (h *ArticleHandler) DeleteArticle(c *fiber.Ctx) error {
id := c.Params("article_id")
if id == "" {
return utils.BadRequest(c, "Article ID is required")
}
err := h.articleService.DeleteArticle(c.Context(), id)
if err != nil {
if isNotFoundError(err) {
return utils.NotFound(c, err.Error())
}
return utils.InternalServerError(c, err.Error())
}
return utils.Success(c, "Article deleted successfully")
}
func isNotFoundError(err error) bool {
return err != nil && (err.Error() == "article not found" ||
err.Error() == "failed to find article: record not found" ||
false)
}

View File

@ -0,0 +1,148 @@
package article
import (
"context"
"errors"
"fmt"
"rijig/model"
"gorm.io/gorm"
)
type ArticleRepository interface {
CreateArticle(ctx context.Context, article *model.Article) error
FindArticleByID(ctx context.Context, id string) (*model.Article, error)
FindAllArticles(ctx context.Context, page, limit int) ([]model.Article, int64, error)
UpdateArticle(ctx context.Context, id string, article *model.Article) error
DeleteArticle(ctx context.Context, id string) error
ArticleExists(ctx context.Context, id string) (bool, error)
}
type articleRepository struct {
db *gorm.DB
}
func NewArticleRepository(db *gorm.DB) ArticleRepository {
return &articleRepository{db: db}
}
func (r *articleRepository) CreateArticle(ctx context.Context, article *model.Article) error {
if article == nil {
return errors.New("article cannot be nil")
}
if err := r.db.WithContext(ctx).Create(article).Error; err != nil {
return fmt.Errorf("failed to create article: %w", err)
}
return nil
}
func (r *articleRepository) FindArticleByID(ctx context.Context, id string) (*model.Article, error) {
if id == "" {
return nil, errors.New("article ID cannot be empty")
}
var article model.Article
err := r.db.WithContext(ctx).Where("id = ?", id).First(&article).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("article with ID %s not found", id)
}
return nil, fmt.Errorf("failed to fetch article: %w", err)
}
return &article, nil
}
func (r *articleRepository) FindAllArticles(ctx context.Context, page, limit int) ([]model.Article, int64, error) {
var articles []model.Article
var total int64
if page < 0 || limit < 0 {
return nil, 0, errors.New("page and limit must be non-negative")
}
if err := r.db.WithContext(ctx).Model(&model.Article{}).Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count articles: %w", err)
}
query := r.db.WithContext(ctx).Model(&model.Article{})
if page > 0 && limit > 0 {
offset := (page - 1) * limit
query = query.Offset(offset).Limit(limit)
}
if err := query.Find(&articles).Error; err != nil {
return nil, 0, fmt.Errorf("failed to fetch articles: %w", err)
}
return articles, total, nil
}
func (r *articleRepository) UpdateArticle(ctx context.Context, id string, article *model.Article) error {
if id == "" {
return errors.New("article ID cannot be empty")
}
if article == nil {
return errors.New("article cannot be nil")
}
exists, err := r.ArticleExists(ctx, id)
if err != nil {
return fmt.Errorf("failed to check article existence: %w", err)
}
if !exists {
return fmt.Errorf("article with ID %s not found", id)
}
result := r.db.WithContext(ctx).Model(&model.Article{}).Where("id = ?", id).Updates(article)
if result.Error != nil {
return fmt.Errorf("failed to update article: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("no rows affected when updating article with ID %s", id)
}
return nil
}
func (r *articleRepository) DeleteArticle(ctx context.Context, id string) error {
if id == "" {
return errors.New("article ID cannot be empty")
}
exists, err := r.ArticleExists(ctx, id)
if err != nil {
return fmt.Errorf("failed to check article existence: %w", err)
}
if !exists {
return fmt.Errorf("article with ID %s not found", id)
}
result := r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.Article{})
if result.Error != nil {
return fmt.Errorf("failed to delete article: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("no rows affected when deleting article with ID %s", id)
}
return nil
}
func (r *articleRepository) ArticleExists(ctx context.Context, id string) (bool, error) {
if id == "" {
return false, errors.New("article ID cannot be empty")
}
var count int64
err := r.db.WithContext(ctx).Model(&model.Article{}).Where("id = ?", id).Count(&count).Error
if err != nil {
return false, fmt.Errorf("failed to check article existence: %w", err)
}
return count > 0, nil
}

View File

@ -0,0 +1,26 @@
package article
import (
"rijig/config"
"rijig/internal/handler"
"rijig/internal/repositories"
"rijig/internal/services"
"rijig/middleware"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
func ArticleRouter(api fiber.Router) {
articleRepo := repositories.NewArticleRepository(config.DB)
articleService := services.NewArticleService(articleRepo)
articleHandler := handler.NewArticleHandler(articleService)
articleAPI := api.Group("/article")
articleAPI.Post("/create", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.CreateArticle)
articleAPI.Get("/view", articleHandler.GetAllArticles)
articleAPI.Get("/view/:article_id", articleHandler.GetArticleByID)
articleAPI.Put("/update/:article_id", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.UpdateArticle)
articleAPI.Delete("/delete/:article_id", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.DeleteArticle)
}

View File

@ -0,0 +1,337 @@
package article
import (
"context"
"fmt"
"log"
"mime/multipart"
"os"
"path/filepath"
"time"
"rijig/model"
"rijig/utils"
"github.com/google/uuid"
)
type ArticleService interface {
CreateArticle(ctx context.Context, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error)
GetAllArticles(ctx context.Context, page, limit int) ([]ArticleResponseDTO, int64, error)
GetArticleByID(ctx context.Context, id string) (*ArticleResponseDTO, error)
UpdateArticle(ctx context.Context, id string, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error)
DeleteArticle(ctx context.Context, id string) error
}
type articleService struct {
articleRepo ArticleRepository
}
func NewArticleService(articleRepo ArticleRepository) ArticleService {
return &articleService{articleRepo}
}
func (s *articleService) transformToDTO(article *model.Article) (*ArticleResponseDTO, error) {
publishedAt, err := utils.FormatDateToIndonesianFormat(article.PublishedAt)
if err != nil {
publishedAt = ""
}
updatedAt, err := utils.FormatDateToIndonesianFormat(article.UpdatedAt)
if err != nil {
updatedAt = ""
}
return &ArticleResponseDTO{
ID: article.ID,
Title: article.Title,
CoverImage: article.CoverImage,
Author: article.Author,
Heading: article.Heading,
Content: article.Content,
PublishedAt: publishedAt,
UpdatedAt: updatedAt,
}, nil
}
func (s *articleService) transformToDTOs(articles []model.Article) ([]ArticleResponseDTO, error) {
var articleDTOs []ArticleResponseDTO
for _, article := range articles {
dto, err := s.transformToDTO(&article)
if err != nil {
return nil, fmt.Errorf("failed to transform article %s: %w", article.ID, err)
}
articleDTOs = append(articleDTOs, *dto)
}
return articleDTOs, nil
}
func (s *articleService) saveCoverArticle(coverArticle *multipart.FileHeader) (string, error) {
if coverArticle == nil {
return "", fmt.Errorf("cover image is required")
}
pathImage := "/uploads/articles/"
coverArticleDir := "./public" + os.Getenv("BASE_URL") + pathImage
if err := os.MkdirAll(coverArticleDir, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to create directory for cover article: %w", err)
}
allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true}
extension := filepath.Ext(coverArticle.Filename)
if !allowedExtensions[extension] {
return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, .png, and .svg are allowed")
}
coverArticleFileName := fmt.Sprintf("%s_coverarticle%s", uuid.New().String(), extension)
coverArticlePath := filepath.Join(coverArticleDir, coverArticleFileName)
src, err := coverArticle.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %w", err)
}
defer src.Close()
dst, err := os.Create(coverArticlePath)
if err != nil {
return "", fmt.Errorf("failed to create cover article file: %w", err)
}
defer dst.Close()
if _, err := dst.ReadFrom(src); err != nil {
return "", fmt.Errorf("failed to save cover article: %w", err)
}
return fmt.Sprintf("%s%s", pathImage, coverArticleFileName), nil
}
func (s *articleService) deleteCoverArticle(imagePath string) error {
if imagePath == "" {
return nil
}
baseDir := "./public/" + os.Getenv("BASE_URL")
absolutePath := baseDir + imagePath
if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
log.Printf("Image file not found (already deleted?): %s", absolutePath)
return nil
}
if err := os.Remove(absolutePath); err != nil {
return fmt.Errorf("failed to delete image: %w", err)
}
log.Printf("Image deleted successfully: %s", absolutePath)
return nil
}
func (s *articleService) invalidateArticleCache(articleID string) {
articleCacheKey := fmt.Sprintf("article:%s", articleID)
if err := utils.DeleteCache(articleCacheKey); err != nil {
log.Printf("Error deleting article cache: %v", err)
}
if err := utils.ScanAndDelete("articles:*"); err != nil {
log.Printf("Error deleting articles cache: %v", err)
}
}
func (s *articleService) CreateArticle(ctx context.Context, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error) {
coverArticlePath, err := s.saveCoverArticle(coverImage)
if err != nil {
return nil, fmt.Errorf("failed to save cover image: %w", err)
}
article := model.Article{
Title: request.Title,
CoverImage: coverArticlePath,
Author: request.Author,
Heading: request.Heading,
Content: request.Content,
}
if err := s.articleRepo.CreateArticle(ctx, &article); err != nil {
if deleteErr := s.deleteCoverArticle(coverArticlePath); deleteErr != nil {
log.Printf("Failed to clean up image after create failure: %v", deleteErr)
}
return nil, fmt.Errorf("failed to create article: %w", err)
}
articleDTO, err := s.transformToDTO(&article)
if err != nil {
return nil, fmt.Errorf("failed to transform article: %w", err)
}
cacheKey := fmt.Sprintf("article:%s", article.ID)
if err := utils.SetCache(cacheKey, articleDTO, time.Hour*24); err != nil {
log.Printf("Error caching article: %v", err)
}
s.invalidateArticleCache("")
return articleDTO, nil
}
func (s *articleService) GetAllArticles(ctx context.Context, page, limit int) ([]ArticleResponseDTO, int64, error) {
var cacheKey string
if page <= 0 || limit <= 0 {
cacheKey = "articles:all"
} else {
cacheKey = fmt.Sprintf("articles:page:%d:limit:%d", page, limit)
}
type CachedArticlesData struct {
Articles []ArticleResponseDTO `json:"articles"`
Total int64 `json:"total"`
}
var cachedData CachedArticlesData
if err := utils.GetCache(cacheKey, &cachedData); err == nil {
return cachedData.Articles, cachedData.Total, nil
}
articles, total, err := s.articleRepo.FindAllArticles(ctx, page, limit)
if err != nil {
return nil, 0, fmt.Errorf("failed to fetch articles: %w", err)
}
articleDTOs, err := s.transformToDTOs(articles)
if err != nil {
return nil, 0, fmt.Errorf("failed to transform articles: %w", err)
}
cacheData := CachedArticlesData{
Articles: articleDTOs,
Total: total,
}
if err := utils.SetCache(cacheKey, cacheData, time.Hour*24); err != nil {
log.Printf("Error caching articles: %v", err)
}
return articleDTOs, total, nil
}
func (s *articleService) GetArticleByID(ctx context.Context, id string) (*ArticleResponseDTO, error) {
if id == "" {
return nil, fmt.Errorf("article ID cannot be empty")
}
cacheKey := fmt.Sprintf("article:%s", id)
var cachedArticle ArticleResponseDTO
if err := utils.GetCache(cacheKey, &cachedArticle); err == nil {
return &cachedArticle, nil
}
article, err := s.articleRepo.FindArticleByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to fetch article: %w", err)
}
articleDTO, err := s.transformToDTO(article)
if err != nil {
return nil, fmt.Errorf("failed to transform article: %w", err)
}
if err := utils.SetCache(cacheKey, articleDTO, time.Hour*24); err != nil {
log.Printf("Error caching article: %v", err)
}
return articleDTO, nil
}
func (s *articleService) UpdateArticle(ctx context.Context, id string, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error) {
if id == "" {
return nil, fmt.Errorf("article ID cannot be empty")
}
existingArticle, err := s.articleRepo.FindArticleByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("article not found: %w", err)
}
oldCoverImage := existingArticle.CoverImage
var newCoverPath string
if coverImage != nil {
newCoverPath, err = s.saveCoverArticle(coverImage)
if err != nil {
return nil, fmt.Errorf("failed to save new cover image: %w", err)
}
}
updatedArticle := &model.Article{
Title: request.Title,
Author: request.Author,
Heading: request.Heading,
Content: request.Content,
CoverImage: existingArticle.CoverImage,
}
if newCoverPath != "" {
updatedArticle.CoverImage = newCoverPath
}
if err := s.articleRepo.UpdateArticle(ctx, id, updatedArticle); err != nil {
if newCoverPath != "" {
s.deleteCoverArticle(newCoverPath)
}
return nil, fmt.Errorf("failed to update article: %w", err)
}
if newCoverPath != "" && oldCoverImage != "" {
if err := s.deleteCoverArticle(oldCoverImage); err != nil {
log.Printf("Warning: failed to delete old cover image: %v", err)
}
}
article, err := s.articleRepo.FindArticleByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to fetch updated article: %w", err)
}
articleDTO, err := s.transformToDTO(article)
if err != nil {
return nil, fmt.Errorf("failed to transform updated article: %w", err)
}
cacheKey := fmt.Sprintf("article:%s", id)
if err := utils.SetCache(cacheKey, articleDTO, time.Hour*24); err != nil {
log.Printf("Error caching updated article: %v", err)
}
s.invalidateArticleCache(id)
return articleDTO, nil
}
func (s *articleService) DeleteArticle(ctx context.Context, id string) error {
if id == "" {
return fmt.Errorf("article ID cannot be empty")
}
article, err := s.articleRepo.FindArticleByID(ctx, id)
if err != nil {
return fmt.Errorf("failed to find article: %w", err)
}
if err := s.articleRepo.DeleteArticle(ctx, id); err != nil {
return fmt.Errorf("failed to delete article: %w", err)
}
if err := s.deleteCoverArticle(article.CoverImage); err != nil {
log.Printf("Warning: failed to delete cover image: %v", err)
}
s.invalidateArticleCache(id)
return nil
}

View File

@ -0,0 +1,362 @@
package authentication
import (
"rijig/utils"
"strings"
"time"
)
type LoginorRegistRequest struct {
Phone string `json:"phone" validate:"required,min=10,max=15"`
RoleName string `json:"role_name"`
}
type VerifyOtpRequest struct {
DeviceID string `json:"device_id" validate:"required"`
RoleName string `json:"role_name" validate:"required,oneof=masyarakat pengepul pengelola"`
Phone string `json:"phone" validate:"required"`
Otp string `json:"otp" validate:"required,len=6"`
}
type CreatePINRequest struct {
PIN string `json:"pin" validate:"required,len=6,numeric"`
ConfirmPIN string `json:"confirm_pin" validate:"required,len=6,numeric"`
}
type VerifyPINRequest struct {
PIN string `json:"pin" validate:"required,len=6,numeric"`
DeviceID string `json:"device_id" validate:"required"`
}
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" validate:"required"`
DeviceID string `json:"device_id" validate:"required"`
UserID string `json:"user_id" validate:"required"`
}
type LogoutRequest struct {
DeviceID string `json:"device_id" validate:"required"`
}
type OTPResponse struct {
Message string `json:"message"`
ExpiresIn int `json:"expires_in"`
Phone string `json:"phone"`
}
type AuthResponse struct {
Message string `json:"message"`
AccessToken string `json:"access_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
TokenType string `json:"token_type,omitempty"`
ExpiresIn int64 `json:"expires_in,omitempty"`
User *UserResponse `json:"user,omitempty"`
RegistrationStatus string `json:"registration_status,omitempty"`
NextStep string `json:"next_step,omitempty"`
SessionID string `json:"session_id,omitempty"`
}
type UserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Phone string `json:"phone"`
Email string `json:"email,omitempty"`
Role string `json:"role"`
RegistrationStatus string `json:"registration_status"`
RegistrationProgress int8 `json:"registration_progress"`
PhoneVerified bool `json:"phone_verified"`
Avatar *string `json:"avatar,omitempty"`
Gender string `json:"gender,omitempty"`
DateOfBirth string `json:"date_of_birth,omitempty"`
PlaceOfBirth string `json:"place_of_birth,omitempty"`
}
type RegistrationStatusResponse struct {
CurrentStep int `json:"current_step"`
TotalSteps int `json:"total_steps"`
CompletedSteps []RegistrationStep `json:"completed_steps"`
NextStep *RegistrationStep `json:"next_step,omitempty"`
RegistrationStatus string `json:"registration_status"`
IsComplete bool `json:"is_complete"`
RequiresApproval bool `json:"requires_approval"`
ApprovalMessage string `json:"approval_message,omitempty"`
}
type RegistrationStep struct {
StepNumber int `json:"step_number"`
Title string `json:"title"`
Description string `json:"description"`
IsRequired bool `json:"is_required"`
IsCompleted bool `json:"is_completed"`
IsActive bool `json:"is_active"`
}
type OTPData struct {
Phone string `json:"phone"`
OTP string `json:"otp"`
UserID string `json:"user_id,omitempty"`
Role string `json:"role"`
RoleID string `json:"role_id,omitempty"`
Type string `json:"type"`
ExpiresAt time.Time `json:"expires_at"`
Attempts int `json:"attempts"`
}
type SessionData struct {
UserID string `json:"user_id"`
DeviceID string `json:"device_id"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
LastSeen time.Time `json:"last_seen"`
IsActive bool `json:"is_active"`
}
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Code string `json:"code,omitempty"`
Details interface{} `json:"details,omitempty"`
}
type ValidationErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Fields map[string]string `json:"fields"`
}
type ApproveRegistrationRequest struct {
UserID string `json:"user_id" validate:"required"`
Message string `json:"message,omitempty"`
}
type RejectRegistrationRequest struct {
UserID string `json:"user_id" validate:"required"`
Reason string `json:"reason" validate:"required"`
}
type PendingRegistrationResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Phone string `json:"phone"`
Role string `json:"role"`
RegistrationData RegistrationData `json:"registration_data"`
SubmittedAt time.Time `json:"submitted_at"`
DocumentsUploaded []DocumentInfo `json:"documents_uploaded"`
}
type RegistrationData struct {
KTPNumber string `json:"ktp_number,omitempty"`
KTPImage string `json:"ktp_image,omitempty"`
FullName string `json:"full_name,omitempty"`
Address string `json:"address,omitempty"`
BusinessName string `json:"business_name,omitempty"`
BusinessType string `json:"business_type,omitempty"`
BusinessAddress string `json:"business_address,omitempty"`
BusinessPhone string `json:"business_phone,omitempty"`
TaxNumber string `json:"tax_number,omitempty"`
BusinessLicense string `json:"business_license,omitempty"`
}
type DocumentInfo struct {
Type string `json:"type"`
FileName string `json:"file_name"`
UploadedAt time.Time `json:"uploaded_at"`
Status string `json:"status"`
FileSize int64 `json:"file_size"`
ContentType string `json:"content_type"`
}
type AuthStatsResponse struct {
TotalUsers int64 `json:"total_users"`
ActiveUsers int64 `json:"active_users"`
PendingRegistrations int64 `json:"pending_registrations"`
UsersByRole map[string]int64 `json:"users_by_role"`
RegistrationStats RegistrationStatsData `json:"registration_stats"`
LoginStats LoginStatsData `json:"login_stats"`
}
type RegistrationStatsData struct {
TotalRegistrations int64 `json:"total_registrations"`
CompletedToday int64 `json:"completed_today"`
CompletedThisWeek int64 `json:"completed_this_week"`
CompletedThisMonth int64 `json:"completed_this_month"`
PendingApproval int64 `json:"pending_approval"`
RejectedRegistrations int64 `json:"rejected_registrations"`
}
type LoginStatsData struct {
TotalLogins int64 `json:"total_logins"`
LoginsToday int64 `json:"logins_today"`
LoginsThisWeek int64 `json:"logins_this_week"`
LoginsThisMonth int64 `json:"logins_this_month"`
UniqueUsersToday int64 `json:"unique_users_today"`
UniqueUsersWeek int64 `json:"unique_users_week"`
UniqueUsersMonth int64 `json:"unique_users_month"`
}
type PaginationRequest struct {
Page int `json:"page" query:"page" validate:"min=1"`
Limit int `json:"limit" query:"limit" validate:"min=1,max=100"`
Sort string `json:"sort" query:"sort"`
Order string `json:"order" query:"order" validate:"oneof=asc desc"`
Search string `json:"search" query:"search"`
Filter string `json:"filter" query:"filter"`
}
type PaginationResponse struct {
Page int `json:"page"`
Limit int `json:"limit"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
}
type PaginatedResponse struct {
Data interface{} `json:"data"`
Pagination PaginationResponse `json:"pagination"`
}
type SMSWebhookRequest struct {
MessageID string `json:"message_id"`
Phone string `json:"phone"`
Status string `json:"status"`
Timestamp string `json:"timestamp"`
}
type RateLimitInfo struct {
Limit int `json:"limit"`
Remaining int `json:"remaining"`
ResetTime time.Time `json:"reset_time"`
RetryAfter time.Duration `json:"retry_after,omitempty"`
}
type StepResponse struct {
UserID string `json:"user_id"`
Role string `json:"role"`
RegistrationStatus string `json:"registration_status"`
RegistrationProgress int `json:"registration_progress"`
NextStep string `json:"next_step"`
}
type RegisterAdminRequest struct {
Name string `json:"name"`
Gender string `json:"gender"`
DateOfBirth string `json:"dateofbirth"`
PlaceOfBirth string `json:"placeofbirth"`
Phone string `json:"phone"`
Email string `json:"email"`
Password string `json:"password"`
PasswordConfirm string `json:"password_confirm"`
}
type LoginAdminRequest struct {
Email string `json:"email"`
Password string `json:"password"`
DeviceID string `json:"device_id"`
}
func (r *LoginorRegistRequest) ValidateLoginorRegistRequest() (map[string][]string, bool) {
errors := make(map[string][]string)
if !utils.IsValidPhoneNumber(r.Phone) {
errors["phone"] = append(errors["phone"], "nomor harus dimulai 62.. dan 8-14 digit")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *VerifyOtpRequest) ValidateVerifyOtpRequest() (map[string][]string, bool) {
errors := make(map[string][]string)
if len(strings.TrimSpace(r.DeviceID)) < 10 {
errors["device_id"] = append(errors["device_id"], "Device ID must be at least 10 characters")
}
validRoles := map[string]bool{"masyarakat": true, "pengepul": true, "pengelola": true}
if _, ok := validRoles[r.RoleName]; !ok {
errors["role"] = append(errors["role"], "Role tidak valid, hanya masyarakat, pengepul, atau pengelola")
}
if !utils.IsValidPhoneNumber(r.Phone) {
errors["phone"] = append(errors["phone"], "nomor harus dimulai 62.. dan 8-14 digit")
}
if len(r.Otp) != 4 || !utils.IsNumeric(r.Otp) {
errors["otp"] = append(errors["otp"], "OTP must be 4-digit number")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *LoginAdminRequest) ValidateLoginAdminRequest() (map[string][]string, bool) {
errors := make(map[string][]string)
if !utils.IsValidEmail(r.Email) {
errors["email"] = append(errors["email"], "Invalid email format")
}
if strings.TrimSpace(r.Password) == "" {
errors["password"] = append(errors["password"], "Password is required")
}
if len(strings.TrimSpace(r.DeviceID)) < 10 {
errors["device_id"] = append(errors["device_id"], "Device ID must be at least 10 characters")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *RegisterAdminRequest) ValidateRegisterAdminRequest() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Name) == "" {
errors["name"] = append(errors["name"], "Name is required")
}
if r.Gender != "male" && r.Gender != "female" {
errors["gender"] = append(errors["gender"], "Gender must be either 'male' or 'female'")
}
if strings.TrimSpace(r.DateOfBirth) == "" {
errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth is required")
} else {
_, err := time.Parse("02-01-2006", r.DateOfBirth)
if err != nil {
errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth must be in DD-MM-YYYY format")
}
}
if strings.TrimSpace(r.PlaceOfBirth) == "" {
errors["placeofbirth"] = append(errors["placeofbirth"], "Place of birth is required")
}
if !utils.IsValidPhoneNumber(r.Phone) {
errors["phone"] = append(errors["phone"], "Phone must be valid, has 8-14 digit and start with '62..'")
}
if !utils.IsValidEmail(r.Email) {
errors["email"] = append(errors["email"], "Invalid email format")
}
if !utils.IsValidPassword(r.Password) {
errors["password"] = append(errors["password"], "Password must be at least 8 characters, with uppercase, number, and special character")
}
if r.Password != r.PasswordConfirm {
errors["password_confirm"] = append(errors["password_confirm"], "Passwords do not match")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1,225 @@
package authentication
import (
"rijig/middleware"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
type AuthenticationHandler struct {
service AuthenticationService
}
func NewAuthenticationHandler(service AuthenticationService) *AuthenticationHandler {
return &AuthenticationHandler{service}
}
func (h *AuthenticationHandler) RefreshToken(c *fiber.Ctx) error {
deviceID := c.Get("X-Device-ID")
if deviceID == "" {
return utils.BadRequest(c, "Device ID is required")
}
var body struct {
RefreshToken string `json:"refresh_token"`
}
if err := c.BodyParser(&body); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
if body.RefreshToken == "" {
return utils.BadRequest(c, "Refresh token is required")
}
userID, ok := c.Locals("user_id").(string)
if !ok || userID == "" {
return utils.Unauthorized(c, "Unauthorized or missing user ID")
}
tokenData, err := utils.RefreshAccessToken(userID, deviceID, body.RefreshToken)
if err != nil {
return utils.Unauthorized(c, err.Error())
}
return utils.SuccessWithData(c, "Token refreshed successfully", tokenData)
}
func (h *AuthenticationHandler) GetMe(c *fiber.Ctx) error {
userID, _ := c.Locals("user_id").(string)
role, _ := c.Locals("role").(string)
deviceID, _ := c.Locals("device_id").(string)
regStatus, _ := c.Locals("registration_status").(string)
data := fiber.Map{
"user_id": userID,
"role": role,
"device_id": deviceID,
"registration_status": regStatus,
}
return utils.SuccessWithData(c, "User session data retrieved", data)
}
func (h *AuthenticationHandler) Login(c *fiber.Ctx) error {
var req LoginAdminRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
if errs, ok := req.ValidateLoginAdminRequest(); !ok {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"meta": fiber.Map{
"status": fiber.StatusBadRequest,
"message": "Validation failed",
},
"errors": errs,
})
}
res, err := h.service.LoginAdmin(c.Context(), &req)
if err != nil {
return utils.Unauthorized(c, err.Error())
}
return utils.SuccessWithData(c, "Login successful", res)
}
func (h *AuthenticationHandler) Register(c *fiber.Ctx) error {
var req RegisterAdminRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
if errs, ok := req.ValidateRegisterAdminRequest(); !ok {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"meta": fiber.Map{
"status": fiber.StatusBadRequest,
"message": "periksa lagi inputan",
},
"errors": errs,
})
}
err := h.service.RegisterAdmin(c.Context(), &req)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.Success(c, "Registration successful, Please login")
}
func (h *AuthenticationHandler) RequestOtpHandler(c *fiber.Ctx) error {
var req LoginorRegistRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
if errs, ok := req.ValidateLoginorRegistRequest(); !ok {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"meta": fiber.Map{
"status": fiber.StatusBadRequest,
"message": "Input tidak valid",
},
"errors": errs,
})
}
_, err := h.service.SendLoginOTP(c.Context(), &req)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.Success(c, "OTP sent successfully")
}
func (h *AuthenticationHandler) VerifyOtpHandler(c *fiber.Ctx) error {
var req VerifyOtpRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
if errs, ok := req.ValidateVerifyOtpRequest(); !ok {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"meta": fiber.Map{"status": fiber.StatusBadRequest, "message": "Validation error"},
"errors": errs,
})
}
stepResp, err := h.service.VerifyLoginOTP(c.Context(), &req)
if err != nil {
return utils.BadRequest(c, err.Error())
}
return utils.SuccessWithData(c, "OTP verified successfully", stepResp)
}
func (h *AuthenticationHandler) RequestOtpRegistHandler(c *fiber.Ctx) error {
var req LoginorRegistRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
if errs, ok := req.ValidateLoginorRegistRequest(); !ok {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"meta": fiber.Map{
"status": fiber.StatusBadRequest,
"message": "Input tidak valid",
},
"errors": errs,
})
}
_, err := h.service.SendRegistrationOTP(c.Context(), &req)
if err != nil {
return utils.Forbidden(c, err.Error())
}
return utils.Success(c, "OTP sent successfully")
}
func (h *AuthenticationHandler) VerifyOtpRegistHandler(c *fiber.Ctx) error {
var req VerifyOtpRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
if errs, ok := req.ValidateVerifyOtpRequest(); !ok {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"meta": fiber.Map{"status": fiber.StatusBadRequest, "message": "Validation error"},
"errors": errs,
})
}
stepResp, err := h.service.VerifyRegistrationOTP(c.Context(), &req)
if err != nil {
return utils.BadRequest(c, err.Error())
}
return utils.SuccessWithData(c, "OTP verified successfully", stepResp)
}
func (h *AuthenticationHandler) LogoutAuthentication(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
// deviceID := c.Get("Device-ID")
// if deviceID == "" {
// return utils.BadRequest(c, "Device ID is required")
// }
err = h.service.LogoutAuthentication(c.Context(), claims.UserID, claims.DeviceID)
if err != nil {
return utils.InternalServerError(c, "Failed to logout")
}
return utils.Success(c, "Logout successful")
}

View File

@ -0,0 +1,86 @@
package authentication
import (
"context"
"rijig/model"
"gorm.io/gorm"
)
type AuthenticationRepository interface {
FindUserByPhone(ctx context.Context, phone string) (*model.User, error)
FindUserByPhoneAndRole(ctx context.Context, phone, rolename string) (*model.User, error)
FindUserByEmail(ctx context.Context, email string) (*model.User, error)
FindUserByID(ctx context.Context, userID string) (*model.User, error)
CreateUser(ctx context.Context, user *model.User) error
UpdateUser(ctx context.Context, user *model.User) error
PatchUser(ctx context.Context, userID string, updates map[string]interface{}) error
}
type authenticationRepository struct {
db *gorm.DB
}
func NewAuthenticationRepository(db *gorm.DB) AuthenticationRepository {
return &authenticationRepository{db}
}
func (r *authenticationRepository) FindUserByPhone(ctx context.Context, phone string) (*model.User, error) {
var user model.User
if err := r.db.WithContext(ctx).
Preload("Role").
Where("phone = ?", phone).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func (r *authenticationRepository) FindUserByPhoneAndRole(ctx context.Context, phone, rolename string) (*model.User, error) {
var user model.User
if err := r.db.WithContext(ctx).
Preload("Role").
Joins("JOIN roles AS role ON role.id = users.role_id").
Where("users.phone = ? AND role.role_name = ?", phone, rolename).
First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func (r *authenticationRepository) FindUserByEmail(ctx context.Context, email string) (*model.User, error) {
var user model.User
if err := r.db.WithContext(ctx).
Preload("Role").
Where("email = ?", email).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func (r *authenticationRepository) FindUserByID(ctx context.Context, userID string) (*model.User, error) {
var user model.User
if err := r.db.WithContext(ctx).
Preload("Role").
First(&user, "id = ?", userID).Error; err != nil {
return nil, err
}
return &user, nil
}
func (r *authenticationRepository) CreateUser(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).Create(user).Error
}
func (r *authenticationRepository) UpdateUser(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).
Model(&model.User{}).
Where("id = ?", user.ID).
Updates(user).Error
}
func (r *authenticationRepository) PatchUser(ctx context.Context, userID string, updates map[string]interface{}) error {
return r.db.WithContext(ctx).
Model(&model.User{}).
Where("id = ?", userID).
Updates(updates).Error
}

View File

@ -0,0 +1,40 @@
package authentication
import (
"rijig/config"
"rijig/internal/role"
"rijig/middleware"
"github.com/gofiber/fiber/v2"
)
func AuthenticationRouter(api fiber.Router) {
repoAuth := NewAuthenticationRepository(config.DB)
repoRole := role.NewRoleRepository(config.DB)
authService := NewAuthenticationService(repoAuth, repoRole)
authHandler := NewAuthenticationHandler(authService)
authRoute := api.Group("/auth")
authRoute.Post("/refresh-token",
middleware.AuthMiddleware(),
middleware.DeviceValidation(),
authHandler.RefreshToken,
)
// authRoute.Get("/me",
// middleware.AuthMiddleware(),
// middleware.CheckRefreshTokenTTL(30*time.Second),
// middleware.RequireApprovedRegistration(),
// authHandler.GetMe,
// )
authRoute.Post("/login/admin", authHandler.Login)
authRoute.Post("/register/admin", authHandler.Register)
authRoute.Post("/request-otp", authHandler.RequestOtpHandler)
authRoute.Post("/verif-otp", authHandler.VerifyOtpHandler)
authRoute.Post("/request-otp/register", authHandler.RequestOtpRegistHandler)
authRoute.Post("/verif-otp/register", authHandler.VerifyOtpRegistHandler)
authRoute.Post("/logout", middleware.AuthMiddleware(), authHandler.LogoutAuthentication)
}

View File

@ -0,0 +1,382 @@
package authentication
import (
"context"
"fmt"
"strings"
"time"
"rijig/internal/role"
"rijig/model"
"rijig/utils"
)
type AuthenticationService interface {
LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*AuthResponse, error)
RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) error
SendRegistrationOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error)
VerifyRegistrationOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error)
SendLoginOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error)
VerifyLoginOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error)
LogoutAuthentication(ctx context.Context, userID, deviceID string) error
}
type authenticationService struct {
authRepo AuthenticationRepository
roleRepo role.RoleRepository
}
func NewAuthenticationService(authRepo AuthenticationRepository, roleRepo role.RoleRepository) AuthenticationService {
return &authenticationService{authRepo, roleRepo}
}
func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*AuthResponse, error) {
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
if user.Role == nil || user.Role.RoleName != "administrator" {
return nil, fmt.Errorf("user not found: %w", err)
}
if user.RegistrationStatus != "completed" {
return nil, fmt.Errorf("user not found: %w", err)
}
if !utils.CompareHashAndPlainText(user.Password, req.Password) {
return nil, fmt.Errorf("user not found: %w", err)
}
token, err := utils.GenerateTokenPair(user.ID, user.Role.RoleName, req.DeviceID, user.RegistrationStatus, int(user.RegistrationProgress))
if err != nil {
return nil, fmt.Errorf("failed to generate token: %w", err)
}
return &AuthResponse{
Message: "login berhasil",
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
RegistrationStatus: user.RegistrationStatus,
SessionID: token.SessionID,
}, nil
}
func (s *authenticationService) RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) error {
existingUser, _ := s.authRepo.FindUserByEmail(ctx, req.Email)
if existingUser != nil {
return fmt.Errorf("email already in use")
}
hashedPassword, err := utils.HashingPlainText(req.Password)
if err != nil {
return fmt.Errorf("failed to hash password: %w", err)
}
role, err := s.roleRepo.FindRoleByName(ctx, "administrator")
if err != nil {
return fmt.Errorf("role name not found: %w", err)
}
user := &model.User{
Name: req.Name,
Phone: req.Phone,
Email: req.Email,
Gender: req.Gender,
Dateofbirth: req.DateOfBirth,
Placeofbirth: req.PlaceOfBirth,
Password: hashedPassword,
RoleID: role.ID,
RegistrationStatus: "completed",
}
if err := s.authRepo.CreateUser(ctx, user); err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
return nil
}
func (s *authenticationService) SendRegistrationOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) {
existingUser, err := s.authRepo.FindUserByPhoneAndRole(ctx, req.Phone, strings.ToLower(req.RoleName))
if err == nil && existingUser != nil {
return nil, fmt.Errorf("nomor telepon dengan role %s sudah terdaftar", req.RoleName)
}
roleData, err := s.roleRepo.FindRoleByName(ctx, req.RoleName)
if err != nil {
return nil, fmt.Errorf("role tidak valid")
}
rateLimitKey := fmt.Sprintf("otp_limit:%s", req.Phone)
if isRateLimited(rateLimitKey, 3, 5*time.Minute) {
return nil, fmt.Errorf("terlalu banyak permintaan OTP, coba lagi dalam 5 menit")
}
otp, err := utils.GenerateOTP()
if err != nil {
return nil, fmt.Errorf("gagal generate OTP: %v", err)
}
otpKey := fmt.Sprintf("otp:%s:register", req.Phone)
otpData := OTPData{
Phone: req.Phone,
OTP: otp,
Role: req.RoleName,
RoleID: roleData.ID,
Type: "register",
Attempts: 0,
}
err = utils.SetCacheWithTTL(otpKey, otpData, 1*time.Minute)
if err != nil {
return nil, fmt.Errorf("gagal menyimpan OTP: %v", err)
}
err = sendOTPViaSMS(req.Phone, otp)
if err != nil {
return nil, fmt.Errorf("gagal mengirim OTP: %v", err)
}
return &OTPResponse{
Message: "OTP berhasil dikirim",
ExpiresIn: 60,
Phone: maskPhoneNumber(req.Phone),
}, nil
}
func (s *authenticationService) VerifyRegistrationOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error) {
otpKey := fmt.Sprintf("otp:%s:register", req.Phone)
var otpData OTPData
err := utils.GetCache(otpKey, &otpData)
if err != nil {
return nil, fmt.Errorf("OTP tidak ditemukan atau sudah kadaluarsa")
}
if otpData.Attempts >= 3 {
utils.DeleteCache(otpKey)
return nil, fmt.Errorf("terlalu banyak percobaan, silakan minta OTP baru")
}
if otpData.OTP != req.Otp {
otpData.Attempts++
utils.SetCache(otpKey, otpData, time.Until(otpData.ExpiresAt))
return nil, fmt.Errorf("kode OTP salah")
}
if otpData.Role != req.RoleName {
return nil, fmt.Errorf("role tidak sesuai")
}
user := &model.User{
Phone: req.Phone,
PhoneVerified: true,
RoleID: otpData.RoleID,
RegistrationStatus: utils.RegStatusIncomplete,
RegistrationProgress: 0,
Name: "",
Gender: "",
Dateofbirth: "",
Placeofbirth: "",
}
err = s.authRepo.CreateUser(ctx, user)
if err != nil {
return nil, fmt.Errorf("gagal membuat user: %v", err)
}
utils.DeleteCache(otpKey)
tokenResponse, err := utils.GenerateTokenPair(
user.ID,
req.RoleName,
req.DeviceID,
user.RegistrationStatus,
int(user.RegistrationProgress),
)
if err != nil {
return nil, fmt.Errorf("gagal generate token: %v", err)
}
nextStep := utils.GetNextRegistrationStep(req.RoleName, int(user.RegistrationProgress),user.RegistrationStatus)
return &AuthResponse{
Message: "Registrasi berhasil",
AccessToken: tokenResponse.AccessToken,
RefreshToken: tokenResponse.RefreshToken,
TokenType: string(tokenResponse.TokenType),
ExpiresIn: tokenResponse.ExpiresIn,
RegistrationStatus: user.RegistrationStatus,
NextStep: nextStep,
SessionID: tokenResponse.SessionID,
}, nil
}
func (s *authenticationService) SendLoginOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) {
user, err := s.authRepo.FindUserByPhone(ctx, req.Phone)
if err != nil {
return nil, fmt.Errorf("nomor telepon tidak terdaftar")
}
if !user.PhoneVerified {
return nil, fmt.Errorf("nomor telepon belum diverifikasi")
}
rateLimitKey := fmt.Sprintf("otp_limit:%s", req.Phone)
if isRateLimited(rateLimitKey, 3, 5*time.Minute) {
return nil, fmt.Errorf("terlalu banyak permintaan OTP, coba lagi dalam 5 menit")
}
otp, err := utils.GenerateOTP()
if err != nil {
return nil, fmt.Errorf("gagal generate OTP: %v", err)
}
otpKey := fmt.Sprintf("otp:%s:login", req.Phone)
otpData := OTPData{
Phone: req.Phone,
OTP: otp,
UserID: user.ID,
Role: user.Role.RoleName,
Type: "login",
Attempts: 0,
}
err = utils.SetCacheWithTTL(otpKey, otpData, 1*time.Minute)
if err != nil {
return nil, fmt.Errorf("gagal menyimpan OTP: %v", err)
}
err = sendOTPViaSMS(req.Phone, otp)
if err != nil {
return nil, fmt.Errorf("gagal mengirim OTP: %v", err)
}
return &OTPResponse{
Message: "OTP berhasil dikirim",
ExpiresIn: 300,
Phone: maskPhoneNumber(req.Phone),
}, nil
}
func (s *authenticationService) VerifyLoginOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error) {
otpKey := fmt.Sprintf("otp:%s:login", req.Phone)
var otpData OTPData
err := utils.GetCache(otpKey, &otpData)
if err != nil {
return nil, fmt.Errorf("OTP tidak ditemukan atau sudah kadaluarsa")
}
if otpData.Attempts >= 3 {
utils.DeleteCache(otpKey)
return nil, fmt.Errorf("terlalu banyak percobaan, silakan minta OTP baru")
}
if otpData.OTP != req.Otp {
otpData.Attempts++
utils.SetCache(otpKey, otpData, time.Until(otpData.ExpiresAt))
return nil, fmt.Errorf("kode OTP salah")
}
user, err := s.authRepo.FindUserByID(ctx, otpData.UserID)
if err != nil {
return nil, fmt.Errorf("user tidak ditemukan")
}
utils.DeleteCache(otpKey)
tokenResponse, err := utils.GenerateTokenPair(
user.ID,
user.Role.RoleName,
req.DeviceID,
"pin_verification_required",
int(user.RegistrationProgress),
)
if err != nil {
return nil, fmt.Errorf("gagal generate token: %v", err)
}
return &AuthResponse{
Message: "OTP berhasil diverifikasi, silakan masukkan PIN",
AccessToken: tokenResponse.AccessToken,
RefreshToken: tokenResponse.RefreshToken,
TokenType: string(tokenResponse.TokenType),
ExpiresIn: tokenResponse.ExpiresIn,
User: convertUserToResponse(user),
RegistrationStatus: user.RegistrationStatus,
NextStep: "Masukkan PIN",
SessionID: tokenResponse.SessionID,
}, nil
}
func (s *authenticationService) LogoutAuthentication(ctx context.Context, userID, deviceID string) error {
if err := utils.RevokeRefreshToken(userID, deviceID); err != nil {
return fmt.Errorf("failed to revoke token: %w", err)
}
return nil
}
func maskPhoneNumber(phone string) string {
if len(phone) < 4 {
return phone
}
return phone[:4] + strings.Repeat("*", len(phone)-8) + phone[len(phone)-4:]
}
func isRateLimited(key string, maxAttempts int, duration time.Duration) bool {
var count int
err := utils.GetCache(key, &count)
if err != nil {
count = 0
}
if count >= maxAttempts {
return true
}
count++
utils.SetCache(key, count, duration)
return false
}
func sendOTPViaSMS(phone, otp string) error {
fmt.Printf("Sending OTP %s to %s\n", otp, phone)
return nil
}
func convertUserToResponse(user *model.User) *UserResponse {
return &UserResponse{
ID: user.ID,
Name: user.Name,
Phone: user.Phone,
Email: user.Email,
Role: user.Role.RoleName,
RegistrationStatus: user.RegistrationStatus,
RegistrationProgress: user.RegistrationProgress,
PhoneVerified: user.PhoneVerified,
Avatar: user.Avatar,
}
}
func IsRegistrationComplete(role string, progress int) bool {
switch role {
case "masyarakat":
return progress >= 1
case "pengepul":
return progress >= 2
case "pengelola":
return progress >= 3
}
return false
}

View File

@ -0,0 +1 @@
package cart

View File

@ -0,0 +1 @@
package cart

View File

@ -0,0 +1 @@
package cart

View File

@ -0,0 +1 @@
package cart

View File

@ -0,0 +1 @@
package cart

View File

@ -0,0 +1 @@
package collector

View File

@ -0,0 +1 @@
package collector

View File

@ -0,0 +1 @@
package collector

View File

@ -0,0 +1 @@
package collector

View File

@ -0,0 +1 @@
package collector

View File

@ -0,0 +1,62 @@
package company
import (
"rijig/utils"
"strings"
)
type ResponseCompanyProfileDTO struct {
ID string `json:"id"`
UserID string `json:"userId"`
CompanyName string `json:"company_name"`
CompanyAddress string `json:"company_address"`
CompanyPhone string `json:"company_phone"`
CompanyEmail string `json:"company_email"`
CompanyLogo string `json:"company_logo,omitempty"`
CompanyWebsite string `json:"company_website,omitempty"`
TaxID string `json:"taxId,omitempty"`
FoundedDate string `json:"founded_date,omitempty"`
CompanyType string `json:"company_type,omitempty"`
CompanyDescription string `json:"company_description"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type RequestCompanyProfileDTO struct {
CompanyName string `json:"company_name"`
CompanyAddress string `json:"company_address"`
CompanyPhone string `json:"company_phone"`
CompanyEmail string `json:"company_email"`
CompanyLogo string `json:"company_logo,omitempty"`
CompanyWebsite string `json:"company_website,omitempty"`
TaxID string `json:"taxId,omitempty"`
FoundedDate string `json:"founded_date,omitempty"`
CompanyType string `json:"company_type,omitempty"`
CompanyDescription string `json:"company_description"`
}
func (r *RequestCompanyProfileDTO) ValidateCompanyProfileInput() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.CompanyName) == "" {
errors["company_Name"] = append(errors["company_name"], "Company name is required")
}
if strings.TrimSpace(r.CompanyAddress) == "" {
errors["company_Address"] = append(errors["company_address"], "Company address is required")
}
if !utils.IsValidPhoneNumber(r.CompanyPhone) {
errors["company_Phone"] = append(errors["company_phone"], "nomor harus dimulai 62.. dan 8-14 digit")
}
if strings.TrimSpace(r.CompanyDescription) == "" {
errors["company_Description"] = append(errors["company_description"], "Company description is required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1,111 @@
package company
import (
"context"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
type CompanyProfileHandler struct {
service CompanyProfileService
}
func NewCompanyProfileHandler(service CompanyProfileService) *CompanyProfileHandler {
return &CompanyProfileHandler{
service: service,
}
}
func (h *CompanyProfileHandler) CreateCompanyProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(string)
if !ok || userID == "" {
return utils.Unauthorized(c, "User not authenticated")
}
var req RequestCompanyProfileDTO
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "invalid request body")
}
if errors, valid := req.ValidateCompanyProfileInput(); !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "validation failed", errors)
}
res, err := h.service.CreateCompanyProfile(context.Background(), userID, &req)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.SuccessWithData(c, "company profile created successfully", res)
}
func (h *CompanyProfileHandler) GetCompanyProfileByID(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(string)
if !ok || userID == "" {
return utils.Unauthorized(c, "User not authenticated")
}
id := c.Params("id")
if id == "" {
return utils.BadRequest(c, "id is required")
}
res, err := h.service.GetCompanyProfileByID(context.Background(), id)
if err != nil {
return utils.NotFound(c, err.Error())
}
return utils.SuccessWithData(c, "company profile retrieved", res)
}
func (h *CompanyProfileHandler) GetCompanyProfilesByUserID(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(string)
if !ok || userID == "" {
return utils.Unauthorized(c, "User not authenticated")
}
res, err := h.service.GetCompanyProfilesByUserID(context.Background(), userID)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.SuccessWithData(c, "company profiles retrieved", res)
}
func (h *CompanyProfileHandler) UpdateCompanyProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(string)
if !ok || userID == "" {
return utils.Unauthorized(c, "User not authenticated")
}
var req RequestCompanyProfileDTO
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "invalid request body")
}
if errors, valid := req.ValidateCompanyProfileInput(); !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "validation failed", errors)
}
res, err := h.service.UpdateCompanyProfile(context.Background(), userID, &req)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.SuccessWithData(c, "company profile updated", res)
}
func (h *CompanyProfileHandler) DeleteCompanyProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(string)
if !ok || userID == "" {
return utils.Unauthorized(c, "User not authenticated")
}
err := h.service.DeleteCompanyProfile(context.Background(), userID)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.Success(c, "company profile deleted")
}

View File

@ -0,0 +1,89 @@
package company
import (
"context"
"fmt"
"rijig/model"
"gorm.io/gorm"
)
type CompanyProfileRepository interface {
CreateCompanyProfile(ctx context.Context, companyProfile *model.CompanyProfile) (*model.CompanyProfile, error)
GetCompanyProfileByID(ctx context.Context, id string) (*model.CompanyProfile, error)
GetCompanyProfilesByUserID(ctx context.Context, userID string) ([]model.CompanyProfile, error)
UpdateCompanyProfile(ctx context.Context, company *model.CompanyProfile) error
DeleteCompanyProfileByUserID(ctx context.Context, userID string) error
ExistsByUserID(ctx context.Context, userID string) (bool, error)
}
type companyProfileRepository struct {
db *gorm.DB
}
func NewCompanyProfileRepository(db *gorm.DB) CompanyProfileRepository {
return &companyProfileRepository{db}
}
func (r *companyProfileRepository) CreateCompanyProfile(ctx context.Context, companyProfile *model.CompanyProfile) (*model.CompanyProfile, error) {
err := r.db.WithContext(ctx).Create(companyProfile).Error
if err != nil {
return nil, fmt.Errorf("failed to create company profile: %v", err)
}
return companyProfile, nil
}
func (r *companyProfileRepository) GetCompanyProfileByID(ctx context.Context, id string) (*model.CompanyProfile, error) {
var companyProfile model.CompanyProfile
err := r.db.WithContext(ctx).Preload("User").First(&companyProfile, "id = ?", id).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("company profile with ID %s not found", id)
}
return nil, fmt.Errorf("error fetching company profile: %v", err)
}
return &companyProfile, nil
}
func (r *companyProfileRepository) GetCompanyProfilesByUserID(ctx context.Context, userID string) ([]model.CompanyProfile, error) {
var companyProfiles []model.CompanyProfile
err := r.db.WithContext(ctx).Preload("User").Where("user_id = ?", userID).Find(&companyProfiles).Error
if err != nil {
return nil, fmt.Errorf("error fetching company profiles for userID %s: %v", userID, err)
}
return companyProfiles, nil
}
func (r *companyProfileRepository) UpdateCompanyProfile(ctx context.Context, company *model.CompanyProfile) error {
var existing model.CompanyProfile
if err := r.db.WithContext(ctx).First(&existing, "user_id = ?", company.UserID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return fmt.Errorf("company profile not found for user_id %s", company.UserID)
}
return fmt.Errorf("failed to fetch company profile: %v", err)
}
err := r.db.WithContext(ctx).Model(&existing).Updates(company).Error
if err != nil {
return fmt.Errorf("failed to update company profile: %v", err)
}
return nil
}
func (r *companyProfileRepository) DeleteCompanyProfileByUserID(ctx context.Context, userID string) error {
err := r.db.WithContext(ctx).Delete(&model.CompanyProfile{}, "user_id = ?", userID).Error
if err != nil {
return fmt.Errorf("failed to delete company profile: %v", err)
}
return nil
}
func (r *companyProfileRepository) ExistsByUserID(ctx context.Context, userID string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&model.CompanyProfile{}).
Where("user_id = ?", userID).Count(&count).Error
if err != nil {
return false, fmt.Errorf("failed to check existence: %v", err)
}
return count > 0, nil
}

View File

@ -0,0 +1,23 @@
package company
import (
"rijig/config"
"rijig/middleware"
"github.com/gofiber/fiber/v2"
)
func CompanyRouter(api fiber.Router) {
companyProfileRepo := NewCompanyProfileRepository(config.DB)
companyProfileService := NewCompanyProfileService(companyProfileRepo)
companyProfileHandler := NewCompanyProfileHandler(companyProfileService)
companyProfileAPI := api.Group("/companyprofile")
companyProfileAPI.Use(middleware.AuthMiddleware())
companyProfileAPI.Post("/create", companyProfileHandler.CreateCompanyProfile)
companyProfileAPI.Get("/get/:id", companyProfileHandler.GetCompanyProfileByID)
companyProfileAPI.Get("/get", companyProfileHandler.GetCompanyProfilesByUserID)
companyProfileAPI.Put("/update", companyProfileHandler.UpdateCompanyProfile)
companyProfileAPI.Delete("/delete", companyProfileHandler.DeleteCompanyProfile)
}

View File

@ -0,0 +1,136 @@
package company
import (
"context"
"fmt"
"rijig/model"
"rijig/utils"
)
type CompanyProfileService interface {
CreateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error)
GetCompanyProfileByID(ctx context.Context, id string) (*ResponseCompanyProfileDTO, error)
GetCompanyProfilesByUserID(ctx context.Context, userID string) ([]ResponseCompanyProfileDTO, error)
UpdateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error)
DeleteCompanyProfile(ctx context.Context, userID string) error
}
type companyProfileService struct {
companyRepo CompanyProfileRepository
}
func NewCompanyProfileService(companyRepo CompanyProfileRepository) CompanyProfileService {
return &companyProfileService{
companyRepo: companyRepo,
}
}
func FormatResponseCompanyProfile(companyProfile *model.CompanyProfile) (*ResponseCompanyProfileDTO, error) {
createdAt, _ := utils.FormatDateToIndonesianFormat(companyProfile.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(companyProfile.UpdatedAt)
return &ResponseCompanyProfileDTO{
ID: companyProfile.ID,
UserID: companyProfile.UserID,
CompanyName: companyProfile.CompanyName,
CompanyAddress: companyProfile.CompanyAddress,
CompanyPhone: companyProfile.CompanyPhone,
CompanyEmail: companyProfile.CompanyEmail,
CompanyLogo: companyProfile.CompanyLogo,
CompanyWebsite: companyProfile.CompanyWebsite,
TaxID: companyProfile.TaxID,
FoundedDate: companyProfile.FoundedDate,
CompanyType: companyProfile.CompanyType,
CompanyDescription: companyProfile.CompanyDescription,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}, nil
}
func (s *companyProfileService) CreateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error) {
if errors, valid := request.ValidateCompanyProfileInput(); !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
companyProfile := &model.CompanyProfile{
UserID: userID,
CompanyName: request.CompanyName,
CompanyAddress: request.CompanyAddress,
CompanyPhone: request.CompanyPhone,
CompanyEmail: request.CompanyEmail,
CompanyLogo: request.CompanyLogo,
CompanyWebsite: request.CompanyWebsite,
TaxID: request.TaxID,
FoundedDate: request.FoundedDate,
CompanyType: request.CompanyType,
CompanyDescription: request.CompanyDescription,
}
created, err := s.companyRepo.CreateCompanyProfile(ctx, companyProfile)
if err != nil {
return nil, err
}
return FormatResponseCompanyProfile(created)
}
func (s *companyProfileService) GetCompanyProfileByID(ctx context.Context, id string) (*ResponseCompanyProfileDTO, error) {
profile, err := s.companyRepo.GetCompanyProfileByID(ctx, id)
if err != nil {
return nil, err
}
return FormatResponseCompanyProfile(profile)
}
func (s *companyProfileService) GetCompanyProfilesByUserID(ctx context.Context, userID string) ([]ResponseCompanyProfileDTO, error) {
profiles, err := s.companyRepo.GetCompanyProfilesByUserID(ctx, userID)
if err != nil {
return nil, err
}
var responses []ResponseCompanyProfileDTO
for _, p := range profiles {
dto, err := FormatResponseCompanyProfile(&p)
if err != nil {
continue
}
responses = append(responses, *dto)
}
return responses, nil
}
func (s *companyProfileService) UpdateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error) {
if errors, valid := request.ValidateCompanyProfileInput(); !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
company := &model.CompanyProfile{
UserID: userID,
CompanyName: request.CompanyName,
CompanyAddress: request.CompanyAddress,
CompanyPhone: request.CompanyPhone,
CompanyEmail: request.CompanyEmail,
CompanyLogo: request.CompanyLogo,
CompanyWebsite: request.CompanyWebsite,
TaxID: request.TaxID,
FoundedDate: request.FoundedDate,
CompanyType: request.CompanyType,
CompanyDescription: request.CompanyDescription,
}
if err := s.companyRepo.UpdateCompanyProfile(ctx, company); err != nil {
return nil, err
}
updated, err := s.companyRepo.GetCompanyProfilesByUserID(ctx, userID)
if err != nil || len(updated) == 0 {
return nil, fmt.Errorf("failed to retrieve updated company profile")
}
return FormatResponseCompanyProfile(&updated[0])
}
func (s *companyProfileService) DeleteCompanyProfile(ctx context.Context, userID string) error {
return s.companyRepo.DeleteCompanyProfileByUserID(ctx, userID)
}

View File

@ -1,5 +1,5 @@
package handler package handler
/*
import ( import (
"fmt" "fmt"
"log" "log"
@ -24,7 +24,7 @@ func (h *AboutHandler) CreateAbout(c *fiber.Ctx) error {
var request dto.RequestAboutDTO var request dto.RequestAboutDTO
if err := c.BodyParser(&request); err != nil { if err := c.BodyParser(&request); err != nil {
log.Printf("Error parsing request body: %v", err) log.Printf("Error parsing request body: %v", err)
return utils.ErrorResponse(c, "Invalid input data") return utils.ResponseErrorData(c, "Invalid input data")
} }
aboutCoverImage, err := c.FormFile("cover_image") aboutCoverImage, err := c.FormFile("cover_image")
@ -173,3 +173,4 @@ func (h *AboutHandler) DeleteAboutDetail(c *fiber.Ctx) error {
return utils.GenericResponse(c, fiber.StatusOK, "Successfully deleted AboutDetail") return utils.GenericResponse(c, fiber.StatusOK, "Successfully deleted AboutDetail")
} }
*/

View File

@ -1,5 +1,5 @@
package handler package handler
/*
import ( import (
"log" "log"
dto "rijig/dto/auth" dto "rijig/dto/auth"
@ -78,3 +78,4 @@ func (h *AuthAdminHandler) LogoutAdmin(c *fiber.Ctx) error {
return utils.GenericResponse(c, fiber.StatusOK, "Successfully logged out") return utils.GenericResponse(c, fiber.StatusOK, "Successfully logged out")
} }
*/

View File

@ -1,5 +1,5 @@
package handler package handler
/*
import ( import (
"log" "log"
"rijig/dto" "rijig/dto"
@ -79,3 +79,4 @@ func (h *AuthMasyarakatHandler) LogoutHandler(c *fiber.Ctx) error {
return utils.SuccessResponse(c, nil, "Logged out successfully") return utils.SuccessResponse(c, nil, "Logged out successfully")
} }
*/

View File

@ -1,5 +1,5 @@
package handler package handler
/*
import ( import (
"log" "log"
"rijig/dto" "rijig/dto"
@ -79,3 +79,4 @@ func (h *AuthPengepulHandler) LogoutHandler(c *fiber.Ctx) error {
return utils.SuccessResponse(c, nil, "Logged out successfully") return utils.SuccessResponse(c, nil, "Logged out successfully")
} }
*/

View File

@ -1,5 +1,5 @@
package handler package handler
/*
import ( import (
"log" "log"
"rijig/dto" "rijig/dto"
@ -79,3 +79,4 @@ func (h *AuthPengelolaHandler) LogoutHandler(c *fiber.Ctx) error {
return utils.SuccessResponse(c, nil, "Logged out successfully") return utils.SuccessResponse(c, nil, "Logged out successfully")
} }
*/

View File

@ -22,7 +22,7 @@ func (h *RoleHandler) GetRoles(c *fiber.Ctx) error {
// return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: You don't have permission to access this resource") // return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: You don't have permission to access this resource")
// } // }
roles, err := h.RoleService.GetRoles() roles, err := h.RoleService.GetRoles(c.Context())
if err != nil { if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error())
} }
@ -38,7 +38,7 @@ func (h *RoleHandler) GetRoleByID(c *fiber.Ctx) error {
// return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: You don't have permission to access this resource") // return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: You don't have permission to access this resource")
// } // }
role, err := h.RoleService.GetRoleByID(roleID) role, err := h.RoleService.GetRoleByID(c.Context(), roleID)
if err != nil { if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, "role id tidak ditemukan") return utils.GenericResponse(c, fiber.StatusNotFound, "role id tidak ditemukan")
} }

View File

@ -0,0 +1,148 @@
package identitycart
import (
"rijig/utils"
"strings"
)
type ResponseIdentityCardDTO struct {
ID string `json:"id"`
UserID string `json:"userId"`
Identificationumber string `json:"identificationumber"`
Placeofbirth string `json:"placeofbirth"`
Dateofbirth string `json:"dateofbirth"`
Gender string `json:"gender"`
BloodType string `json:"bloodtype"`
Province string `json:"province"`
District string `json:"district"`
SubDistrict string `json:"subdistrict"`
Hamlet string `json:"hamlet"`
Village string `json:"village"`
Neighbourhood string `json:"neighbourhood"`
PostalCode string `json:"postalcode"`
Religion string `json:"religion"`
Maritalstatus string `json:"maritalstatus"`
Job string `json:"job"`
Citizenship string `json:"citizenship"`
Validuntil string `json:"validuntil"`
Cardphoto string `json:"cardphoto"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type RequestIdentityCardDTO struct {
DeviceID string `json:"device_id"`
UserID string `json:"userId"`
Identificationumber string `json:"identificationumber"`
Placeofbirth string `json:"placeofbirth"`
Dateofbirth string `json:"dateofbirth"`
Gender string `json:"gender"`
BloodType string `json:"bloodtype"`
Province string `json:"province"`
District string `json:"district"`
SubDistrict string `json:"subdistrict"`
Hamlet string `json:"hamlet"`
Village string `json:"village"`
Neighbourhood string `json:"neighbourhood"`
PostalCode string `json:"postalcode"`
Religion string `json:"religion"`
Maritalstatus string `json:"maritalstatus"`
Job string `json:"job"`
Citizenship string `json:"citizenship"`
Validuntil string `json:"validuntil"`
Cardphoto string `json:"cardphoto"`
}
func (r *RequestIdentityCardDTO) ValidateIdentityCardInput() (map[string][]string, bool) {
errors := make(map[string][]string)
r.Placeofbirth = strings.ToLower(r.Placeofbirth)
r.Dateofbirth = strings.ToLower(r.Dateofbirth)
r.Gender = strings.ToLower(r.Gender)
r.BloodType = strings.ToUpper(r.BloodType)
r.Province = strings.ToLower(r.Province)
r.District = strings.ToLower(r.District)
r.SubDistrict = strings.ToLower(r.SubDistrict)
r.Hamlet = strings.ToLower(r.Hamlet)
r.Village = strings.ToLower(r.Village)
r.Neighbourhood = strings.ToLower(r.Neighbourhood)
r.PostalCode = strings.ToLower(r.PostalCode)
r.Religion = strings.ToLower(r.Religion)
r.Maritalstatus = strings.ToLower(r.Maritalstatus)
r.Job = strings.ToLower(r.Job)
r.Citizenship = strings.ToLower(r.Citizenship)
r.Validuntil = strings.ToLower(r.Validuntil)
nikData := utils.FetchNIKData(r.Identificationumber)
if strings.ToLower(nikData.Status) != "sukses" {
errors["identificationumber"] = append(errors["identificationumber"], "NIK yang anda masukkan tidak valid")
} else {
if r.Dateofbirth != strings.ToLower(nikData.Ttl) {
errors["dateofbirth"] = append(errors["dateofbirth"], "Tanggal lahir tidak sesuai dengan NIK")
}
if r.Gender != strings.ToLower(nikData.Sex) {
errors["gender"] = append(errors["gender"], "Jenis kelamin tidak sesuai dengan NIK")
}
if r.Province != strings.ToLower(nikData.Provinsi) {
errors["province"] = append(errors["province"], "Provinsi tidak sesuai dengan NIK")
}
if r.District != strings.ToLower(nikData.Kabkot) {
errors["district"] = append(errors["district"], "Kabupaten/Kota tidak sesuai dengan NIK")
}
if r.SubDistrict != strings.ToLower(nikData.Kecamatan) {
errors["subdistrict"] = append(errors["subdistrict"], "Kecamatan tidak sesuai dengan NIK")
}
if r.PostalCode != strings.ToLower(nikData.KodPos) {
errors["postalcode"] = append(errors["postalcode"], "Kode pos tidak sesuai dengan NIK")
}
}
if r.Placeofbirth == "" {
errors["placeofbirth"] = append(errors["placeofbirth"], "Tempat lahir wajib diisi")
}
if r.Hamlet == "" {
errors["hamlet"] = append(errors["hamlet"], "Dusun/RW wajib diisi")
}
if r.Village == "" {
errors["village"] = append(errors["village"], "Desa/Kelurahan wajib diisi")
}
if r.Neighbourhood == "" {
errors["neighbourhood"] = append(errors["neighbourhood"], "RT wajib diisi")
}
if r.Job == "" {
errors["job"] = append(errors["job"], "Pekerjaan wajib diisi")
}
if r.Citizenship == "" {
errors["citizenship"] = append(errors["citizenship"], "Kewarganegaraan wajib diisi")
}
if r.Validuntil == "" {
errors["validuntil"] = append(errors["validuntil"], "Berlaku hingga wajib diisi")
}
validBloodTypes := map[string]bool{"A": true, "B": true, "O": true, "AB": true}
if _, ok := validBloodTypes[r.BloodType]; !ok {
errors["bloodtype"] = append(errors["bloodtype"], "Golongan darah harus A, B, O, atau AB")
}
validReligions := map[string]bool{
"islam": true, "kristen": true, "katolik": true, "hindu": true, "buddha": true, "konghucu": true,
}
if _, ok := validReligions[r.Religion]; !ok {
errors["religion"] = append(errors["religion"], "Agama harus salah satu dari Islam, Kristen, Katolik, Hindu, Buddha, atau Konghucu")
}
if r.Maritalstatus != "kawin" && r.Maritalstatus != "belum kawin" {
errors["maritalstatus"] = append(errors["maritalstatus"], "Status perkawinan harus 'kawin' atau 'belum kawin'")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1,73 @@
package identitycart
import (
"rijig/middleware"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
type IdentityCardHandler struct {
service IdentityCardService
}
func NewIdentityCardHandler(service IdentityCardService) *IdentityCardHandler {
return &IdentityCardHandler{service: service}
}
func (h *IdentityCardHandler) CreateIdentityCardHandler(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
cardPhoto, err := c.FormFile("cardphoto")
if err != nil {
return utils.BadRequest(c, "KTP photo is required")
}
var input RequestIdentityCardDTO
if err := c.BodyParser(&input); err != nil {
return utils.BadRequest(c, "Invalid input format")
}
if errs, valid := input.ValidateIdentityCardInput(); !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Input validation failed", errs)
}
response, err := h.service.CreateIdentityCard(c.Context(), claims.UserID, &input, cardPhoto)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.SuccessWithData(c, "KTP successfully submitted", response)
}
func (h *IdentityCardHandler) GetIdentityByID(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return utils.BadRequest(c, "id is required")
}
result, err := h.service.GetIdentityCardByID(c.Context(), id)
if err != nil {
return utils.NotFound(c, "data not found")
}
return utils.SuccessWithData(c, "success retrieve identity card", result)
}
func (h *IdentityCardHandler) GetIdentityByUserId(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
result, err := h.service.GetIdentityCardsByUserID(c.Context(), claims.UserID)
if err != nil {
return utils.InternalServerError(c, "failed to fetch your identity card data")
}
return utils.SuccessWithData(c, "success retrieve your identity card", result)
}

View File

@ -0,0 +1,64 @@
package identitycart
import (
"context"
"errors"
"fmt"
"log"
"rijig/model"
"gorm.io/gorm"
)
type IdentityCardRepository interface {
CreateIdentityCard(ctx context.Context, identityCard *model.IdentityCard) (*model.IdentityCard, error)
GetIdentityCardByID(ctx context.Context, id string) (*model.IdentityCard, error)
GetIdentityCardsByUserID(ctx context.Context, userID string) ([]model.IdentityCard, error)
UpdateIdentityCard(ctx context.Context, identity *model.IdentityCard) error
}
type identityCardRepository struct {
db *gorm.DB
}
func NewIdentityCardRepository(db *gorm.DB) IdentityCardRepository {
return &identityCardRepository{
db: db,
}
}
func (r *identityCardRepository) CreateIdentityCard(ctx context.Context, identityCard *model.IdentityCard) (*model.IdentityCard, error) {
if err := r.db.WithContext(ctx).Create(identityCard).Error; err != nil {
log.Printf("Error creating identity card: %v", err)
return nil, fmt.Errorf("failed to create identity card: %w", err)
}
return identityCard, nil
}
func (r *identityCardRepository) GetIdentityCardByID(ctx context.Context, id string) (*model.IdentityCard, error) {
var identityCard model.IdentityCard
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&identityCard).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("identity card not found with id %s", id)
}
log.Printf("Error fetching identity card by ID: %v", err)
return nil, fmt.Errorf("error fetching identity card by ID: %w", err)
}
return &identityCard, nil
}
func (r *identityCardRepository) GetIdentityCardsByUserID(ctx context.Context, userID string) ([]model.IdentityCard, error) {
var identityCards []model.IdentityCard
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&identityCards).Error; err != nil {
log.Printf("Error fetching identity cards by userID: %v", err)
return nil, fmt.Errorf("error fetching identity cards by userID: %w", err)
}
return identityCards, nil
}
func (r *identityCardRepository) UpdateIdentityCard(ctx context.Context, identity *model.IdentityCard) error {
return r.db.WithContext(ctx).
Model(&model.IdentityCard{}).
Where("user_id = ?", identity.UserID).
Updates(identity).Error
}

View File

@ -0,0 +1,35 @@
package identitycart
import (
"rijig/config"
"rijig/internal/authentication"
"rijig/middleware"
"github.com/gofiber/fiber/v2"
)
func UserIdentityCardRoute(api fiber.Router) {
identityRepo := NewIdentityCardRepository(config.DB)
authRepo := authentication.NewAuthenticationRepository(config.DB)
identityService := NewIdentityCardService(identityRepo, authRepo)
identityHandler := NewIdentityCardHandler(identityService)
identity := api.Group("/identity")
identity.Post("/create",
middleware.AuthMiddleware(),
middleware.RequireRoles("pengelola", "pengepul"),
identityHandler.CreateIdentityCardHandler,
)
identity.Get("/:id",
middleware.AuthMiddleware(),
middleware.RequireRoles("pengelola", "pengepul"),
identityHandler.GetIdentityByID,
)
identity.Get("/",
middleware.AuthMiddleware(),
middleware.RequireRoles("pengelola", "pengepul"),
identityHandler.GetIdentityByUserId,
)
}

View File

@ -0,0 +1,321 @@
package identitycart
import (
"context"
"fmt"
"log"
"mime/multipart"
"os"
"path/filepath"
"rijig/internal/authentication"
"rijig/model"
"rijig/utils"
"strings"
)
type IdentityCardService interface {
CreateIdentityCard(ctx context.Context, userID string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*authentication.AuthResponse, error)
GetIdentityCardByID(ctx context.Context, id string) (*ResponseIdentityCardDTO, error)
GetIdentityCardsByUserID(ctx context.Context, userID string) ([]ResponseIdentityCardDTO, error)
UpdateIdentityCard(ctx context.Context, userID string, id string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*ResponseIdentityCardDTO, error)
}
type identityCardService struct {
identityRepo IdentityCardRepository
authRepo authentication.AuthenticationRepository
}
func NewIdentityCardService(identityRepo IdentityCardRepository, authRepo authentication.AuthenticationRepository) IdentityCardService {
return &identityCardService{
identityRepo: identityRepo,
authRepo: authRepo,
}
}
func FormatResponseIdentityCard(identityCard *model.IdentityCard) (*ResponseIdentityCardDTO, error) {
createdAt, _ := utils.FormatDateToIndonesianFormat(identityCard.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(identityCard.UpdatedAt)
return &ResponseIdentityCardDTO{
ID: identityCard.ID,
UserID: identityCard.UserID,
Identificationumber: identityCard.Identificationumber,
Placeofbirth: identityCard.Placeofbirth,
Dateofbirth: identityCard.Dateofbirth,
Gender: identityCard.Gender,
BloodType: identityCard.BloodType,
Province: identityCard.Province,
District: identityCard.District,
SubDistrict: identityCard.SubDistrict,
Hamlet: identityCard.Hamlet,
Village: identityCard.Village,
Neighbourhood: identityCard.Neighbourhood,
PostalCode: identityCard.PostalCode,
Religion: identityCard.Religion,
Maritalstatus: identityCard.Maritalstatus,
Job: identityCard.Job,
Citizenship: identityCard.Citizenship,
Validuntil: identityCard.Validuntil,
Cardphoto: identityCard.Cardphoto,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}, nil
}
func (s *identityCardService) saveIdentityCardImage(userID string, cardPhoto *multipart.FileHeader) (string, error) {
pathImage := "/uploads/identitycards/"
cardPhotoDir := "./public" + os.Getenv("BASE_URL") + pathImage
if _, err := os.Stat(cardPhotoDir); os.IsNotExist(err) {
if err := os.MkdirAll(cardPhotoDir, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to create directory for identity card photo: %v", err)
}
}
allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true}
extension := filepath.Ext(cardPhoto.Filename)
if !allowedExtensions[extension] {
return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed")
}
cardPhotoFileName := fmt.Sprintf("%s_cardphoto%s", userID, extension)
cardPhotoPath := filepath.Join(cardPhotoDir, cardPhotoFileName)
src, err := cardPhoto.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %v", err)
}
defer src.Close()
dst, err := os.Create(cardPhotoPath)
if err != nil {
return "", fmt.Errorf("failed to create card photo file: %v", err)
}
defer dst.Close()
if _, err := dst.ReadFrom(src); err != nil {
return "", fmt.Errorf("failed to save card photo: %v", err)
}
cardPhotoURL := fmt.Sprintf("%s%s", pathImage, cardPhotoFileName)
return cardPhotoURL, nil
}
func deleteIdentityCardImage(imagePath string) error {
if imagePath == "" {
return nil
}
baseDir := "./public/" + os.Getenv("BASE_URL")
absolutePath := baseDir + imagePath
if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
return fmt.Errorf("image file not found: %v", err)
}
err := os.Remove(absolutePath)
if err != nil {
return fmt.Errorf("failed to delete image: %v", err)
}
log.Printf("Image deleted successfully: %s", absolutePath)
return nil
}
func (s *identityCardService) CreateIdentityCard(ctx context.Context, userID string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*authentication.AuthResponse, error) {
// Validate essential parameters
if userID == "" {
return nil, fmt.Errorf("userID cannot be empty")
}
if request.DeviceID == "" {
return nil, fmt.Errorf("deviceID cannot be empty")
}
cardPhotoPath, err := s.saveIdentityCardImage(userID, cardPhoto)
if err != nil {
return nil, fmt.Errorf("failed to save card photo: %v", err)
}
identityCard := &model.IdentityCard{
UserID: userID,
Identificationumber: request.Identificationumber,
Placeofbirth: request.Placeofbirth,
Dateofbirth: request.Dateofbirth,
Gender: request.Gender,
BloodType: request.BloodType,
Province: request.Province,
District: request.District,
SubDistrict: request.SubDistrict,
Hamlet: request.Hamlet,
Village: request.Village,
Neighbourhood: request.Neighbourhood,
PostalCode: request.PostalCode,
Religion: request.Religion,
Maritalstatus: request.Maritalstatus,
Job: request.Job,
Citizenship: request.Citizenship,
Validuntil: request.Validuntil,
Cardphoto: cardPhotoPath,
}
_, err = s.identityRepo.CreateIdentityCard(ctx, identityCard)
if err != nil {
log.Printf("Error creating identity card: %v", err)
return nil, fmt.Errorf("failed to create identity card: %v", err)
}
user, err := s.authRepo.FindUserByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to find user: %v", err)
}
// Validate user data
if user.Role.RoleName == "" {
return nil, fmt.Errorf("user role not found")
}
roleName := strings.ToLower(user.Role.RoleName)
// Determine new registration status and progress
var newRegistrationStatus string
var newRegistrationProgress int
switch roleName {
case "pengepul":
newRegistrationProgress = 2
newRegistrationStatus = utils.RegStatusPending
case "pengelola":
newRegistrationProgress = 2
newRegistrationStatus = user.RegistrationStatus
default:
newRegistrationProgress = int(user.RegistrationProgress)
newRegistrationStatus = user.RegistrationStatus
}
// Update user registration progress and status
updates := map[string]interface{}{
"registration_progress": newRegistrationProgress,
"registration_status": newRegistrationStatus,
}
err = s.authRepo.PatchUser(ctx, userID, updates)
if err != nil {
return nil, fmt.Errorf("failed to update user: %v", err)
}
// Debug logging before token generation
log.Printf("Token Generation Parameters:")
log.Printf("- UserID: '%s'", user.ID)
log.Printf("- Role: '%s'", user.Role.RoleName)
log.Printf("- DeviceID: '%s'", request.DeviceID)
log.Printf("- Registration Status: '%s'", newRegistrationStatus)
// Generate token pair with updated status
tokenResponse, err := utils.GenerateTokenPair(
user.ID,
user.Role.RoleName,
request.DeviceID,
newRegistrationStatus,
newRegistrationProgress,
)
if err != nil {
log.Printf("GenerateTokenPair error: %v", err)
return nil, fmt.Errorf("failed to generate token: %v", err)
}
return &authentication.AuthResponse{
Message: "identity card berhasil diunggah, silakan tunggu konfirmasi dari admin dalam 1x24 jam",
AccessToken: tokenResponse.AccessToken,
RefreshToken: tokenResponse.RefreshToken,
TokenType: string(tokenResponse.TokenType),
ExpiresIn: tokenResponse.ExpiresIn,
RegistrationStatus: newRegistrationStatus,
NextStep: tokenResponse.NextStep,
SessionID: tokenResponse.SessionID,
}, nil
}
func (s *identityCardService) GetIdentityCardByID(ctx context.Context, id string) (*ResponseIdentityCardDTO, error) {
identityCard, err := s.identityRepo.GetIdentityCardByID(ctx, id)
if err != nil {
log.Printf("Error fetching identity card: %v", err)
return nil, fmt.Errorf("failed to fetch identity card")
}
return FormatResponseIdentityCard(identityCard)
}
func (s *identityCardService) GetIdentityCardsByUserID(ctx context.Context, userID string) ([]ResponseIdentityCardDTO, error) {
identityCards, err := s.identityRepo.GetIdentityCardsByUserID(ctx, userID)
if err != nil {
log.Printf("Error fetching identity cards by userID: %v", err)
return nil, fmt.Errorf("failed to fetch identity cards by userID")
}
var response []ResponseIdentityCardDTO
for _, card := range identityCards {
dto, _ := FormatResponseIdentityCard(&card)
response = append(response, *dto)
}
return response, nil
}
func (s *identityCardService) UpdateIdentityCard(ctx context.Context, userID string, id string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*ResponseIdentityCardDTO, error) {
errors, valid := request.ValidateIdentityCardInput()
if !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
identityCard, err := s.identityRepo.GetIdentityCardByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("identity card not found: %v", err)
}
if identityCard.Cardphoto != "" {
err := deleteIdentityCardImage(identityCard.Cardphoto)
if err != nil {
return nil, fmt.Errorf("failed to delete old image: %v", err)
}
}
var cardPhotoPath string
if cardPhoto != nil {
cardPhotoPath, err = s.saveIdentityCardImage(userID, cardPhoto)
if err != nil {
return nil, fmt.Errorf("failed to save card photo: %v", err)
}
}
identityCard.Identificationumber = request.Identificationumber
identityCard.Placeofbirth = request.Placeofbirth
identityCard.Dateofbirth = request.Dateofbirth
identityCard.Gender = request.Gender
identityCard.BloodType = request.BloodType
identityCard.Province = request.Province
identityCard.District = request.District
identityCard.SubDistrict = request.SubDistrict
identityCard.Hamlet = request.Hamlet
identityCard.Village = request.Village
identityCard.Neighbourhood = request.Neighbourhood
identityCard.PostalCode = request.PostalCode
identityCard.Religion = request.Religion
identityCard.Maritalstatus = request.Maritalstatus
identityCard.Job = request.Job
identityCard.Citizenship = request.Citizenship
identityCard.Validuntil = request.Validuntil
if cardPhotoPath != "" {
identityCard.Cardphoto = cardPhotoPath
}
if err != nil {
log.Printf("Error updating identity card: %v", err)
return nil, fmt.Errorf("failed to update identity card: %v", err)
}
idcardResponseDTO, _ := FormatResponseIdentityCard(identityCard)
return idcardResponseDTO, nil
}

View File

@ -1,48 +1,49 @@
package repositories package repositories
import ( import (
"context"
"rijig/model" "rijig/model"
"gorm.io/gorm" "gorm.io/gorm"
) )
type RoleRepository interface { type RoleRepository interface {
FindByID(id string) (*model.Role, error) FindByID(ctx context.Context, id string) (*model.Role, error)
FindRoleByName(roleName string) (*model.Role, error) FindRoleByName(ctx context.Context, roleName string) (*model.Role, error)
FindAll() ([]model.Role, error) FindAll(ctx context.Context) ([]model.Role, error)
} }
type roleRepository struct { type roleRepository struct {
DB *gorm.DB db *gorm.DB
} }
func NewRoleRepository(db *gorm.DB) RoleRepository { func NewRoleRepository(db *gorm.DB) RoleRepository {
return &roleRepository{DB: db} return &roleRepository{db}
} }
func (r *roleRepository) FindByID(id string) (*model.Role, error) { func (r *roleRepository) FindByID(ctx context.Context, id string) (*model.Role, error) {
var role model.Role var role model.Role
err := r.DB.Where("id = ?", id).First(&role).Error err := r.db.WithContext(ctx).Where("id = ?", id).First(&role).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &role, nil return &role, nil
} }
func (r *roleRepository) FindAll() ([]model.Role, error) { func (r *roleRepository) FindRoleByName(ctx context.Context, roleName string) (*model.Role, error) {
var role model.Role
err := r.db.WithContext(ctx).Where("role_name = ?", roleName).First(&role).Error
if err != nil {
return nil, err
}
return &role, nil
}
func (r *roleRepository) FindAll(ctx context.Context) ([]model.Role, error) {
var roles []model.Role var roles []model.Role
err := r.DB.Find(&roles).Error err := r.db.WithContext(ctx).Find(&roles).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return roles, nil return roles, nil
} }
func (r *roleRepository) FindRoleByName(roleName string) (*model.Role, error) {
var role model.Role
err := r.DB.Where("role_name = ?", roleName).First(&role).Error
if err != nil {
return nil, err
}
return &role, nil
}

View File

@ -69,7 +69,6 @@ func (r *trashRepository) GetCategoryByID(id string) (*model.TrashCategory, erro
return &category, nil return &category, nil
} }
// spesial code
func (r *trashRepository) GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) { func (r *trashRepository) GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) {
var trash model.TrashCategory var trash model.TrashCategory
if err := config.DB.WithContext(ctx).First(&trash, "id = ?", id).Error; err != nil { if err := config.DB.WithContext(ctx).First(&trash, "id = ?", id).Error; err != nil {

View File

@ -0,0 +1 @@
package requestpickup

View File

@ -0,0 +1 @@
package requestpickup

View File

@ -0,0 +1 @@
package requestpickup

View File

@ -0,0 +1 @@
package requestpickup

View File

@ -0,0 +1 @@
package requestpickup

View File

@ -0,0 +1,8 @@
package role
type RoleResponseDTO struct {
ID string `json:"role_id"`
RoleName string `json:"role_name"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}

View File

@ -0,0 +1,52 @@
package role
import (
"rijig/middleware"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
type RoleHandler struct {
roleService RoleService
}
func NewRoleHandler(roleService RoleService) *RoleHandler {
return &RoleHandler{
roleService: roleService,
}
}
func (h *RoleHandler) GetRoles(c *fiber.Ctx) error {
if _, err := middleware.GetUserFromContext(c); err != nil {
return utils.Unauthorized(c, "Unauthorized access")
}
roles, err := h.roleService.GetRoles(c.Context())
if err != nil {
return utils.InternalServerError(c, "Failed to fetch roles")
}
return utils.SuccessWithData(c, "Roles fetched successfully", roles)
}
func (h *RoleHandler) GetRoleByID(c *fiber.Ctx) error {
if _, err := middleware.GetUserFromContext(c); err != nil {
return utils.Unauthorized(c, "Unauthorized access")
}
roleID := c.Params("role_id")
if roleID == "" {
return utils.BadRequest(c, "Role ID is required")
}
role, err := h.roleService.GetRoleByID(c.Context(), roleID)
if err != nil {
return utils.NotFound(c, "Role not found")
}
return utils.SuccessWithData(c, "Role fetched successfully", role)
}

View File

@ -0,0 +1,49 @@
package role
import (
"context"
"rijig/model"
"gorm.io/gorm"
)
type RoleRepository interface {
FindByID(ctx context.Context, id string) (*model.Role, error)
FindRoleByName(ctx context.Context, roleName string) (*model.Role, error)
FindAll(ctx context.Context) ([]model.Role, error)
}
type roleRepository struct {
db *gorm.DB
}
func NewRoleRepository(db *gorm.DB) RoleRepository {
return &roleRepository{db}
}
func (r *roleRepository) FindByID(ctx context.Context, id string) (*model.Role, error) {
var role model.Role
err := r.db.WithContext(ctx).Where("id = ?", id).First(&role).Error
if err != nil {
return nil, err
}
return &role, nil
}
func (r *roleRepository) FindRoleByName(ctx context.Context, roleName string) (*model.Role, error) {
var role model.Role
err := r.db.WithContext(ctx).Where("role_name = ?", roleName).First(&role).Error
if err != nil {
return nil, err
}
return &role, nil
}
func (r *roleRepository) FindAll(ctx context.Context) ([]model.Role, error) {
var roles []model.Role
err := r.db.WithContext(ctx).Find(&roles).Error
if err != nil {
return nil, err
}
return roles, nil
}

View File

@ -0,0 +1,18 @@
package role
import (
"rijig/config"
"rijig/middleware"
"github.com/gofiber/fiber/v2"
)
func UserRoleRouter(api fiber.Router) {
roleRepo := NewRoleRepository(config.DB)
roleService := NewRoleService(roleRepo)
roleHandler := NewRoleHandler(roleService)
roleRoute := api.Group("/role", middleware.AuthMiddleware())
roleRoute.Get("/", roleHandler.GetRoles)
roleRoute.Get("/:role_id", roleHandler.GetRoleByID)
}

View File

@ -0,0 +1,89 @@
package role
import (
"context"
"fmt"
"time"
"rijig/utils"
)
type RoleService interface {
GetRoles(ctx context.Context) ([]RoleResponseDTO, error)
GetRoleByID(ctx context.Context, roleID string) (*RoleResponseDTO, error)
}
type roleService struct {
RoleRepo RoleRepository
}
func NewRoleService(roleRepo RoleRepository) RoleService {
return &roleService{roleRepo}
}
func (s *roleService) GetRoles(ctx context.Context) ([]RoleResponseDTO, error) {
cacheKey := "roles_list"
var cachedRoles []RoleResponseDTO
err := utils.GetCache(cacheKey, &cachedRoles)
if err == nil {
return cachedRoles, nil
}
roles, err := s.RoleRepo.FindAll(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch roles: %v", err)
}
var roleDTOs []RoleResponseDTO
for _, role := range roles {
createdAt, _ := utils.FormatDateToIndonesianFormat(role.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(role.UpdatedAt)
roleDTOs = append(roleDTOs, RoleResponseDTO{
ID: role.ID,
RoleName: role.RoleName,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
err = utils.SetCache(cacheKey, roleDTOs, time.Hour*24)
if err != nil {
fmt.Printf("Error caching roles data to Redis: %v\n", err)
}
return roleDTOs, nil
}
func (s *roleService) GetRoleByID(ctx context.Context, roleID string) (*RoleResponseDTO, error) {
cacheKey := fmt.Sprintf("role:%s", roleID)
var cachedRole RoleResponseDTO
err := utils.GetCache(cacheKey, &cachedRole)
if err == nil {
return &cachedRole, nil
}
role, err := s.RoleRepo.FindByID(ctx, roleID)
if err != nil {
return nil, fmt.Errorf("role not found: %v", err)
}
createdAt, _ := utils.FormatDateToIndonesianFormat(role.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(role.UpdatedAt)
roleDTO := &RoleResponseDTO{
ID: role.ID,
RoleName: role.RoleName,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
err = utils.SetCache(cacheKey, roleDTO, time.Hour*24)
if err != nil {
fmt.Printf("Error caching role data to Redis: %v\n", err)
}
return roleDTO, nil
}

View File

@ -1,5 +1,5 @@
package service package service
/*
import ( import (
"errors" "errors"
"fmt" "fmt"
@ -189,3 +189,4 @@ func (s *authAdminService) LogoutAdmin(userID, deviceID string) error {
return nil return nil
} }
*/

View File

@ -1,5 +1,5 @@
package service package service
/*
import ( import (
"errors" "errors"
"fmt" "fmt"
@ -168,3 +168,4 @@ func (s *authMasyarakatService) Logout(userID, deviceID string) error {
return nil return nil
} }
*/

View File

@ -1,5 +1,5 @@
package service package service
/*
import ( import (
"errors" "errors"
"fmt" "fmt"
@ -168,3 +168,4 @@ func (s *authPengelolaService) Logout(userID, deviceID string) error {
return nil return nil
} }
*/

View File

@ -1,5 +1,5 @@
package service package service
/*
import ( import (
"errors" "errors"
"fmt" "fmt"
@ -169,3 +169,4 @@ func (s *authPengepulService) Logout(userID, deviceID string) error {
return nil return nil
} }
*/

View File

@ -1,6 +1,7 @@
package services package services
import ( import (
"context"
"fmt" "fmt"
"time" "time"
@ -10,8 +11,8 @@ import (
) )
type RoleService interface { type RoleService interface {
GetRoles() ([]dto.RoleResponseDTO, error) GetRoles(ctx context.Context) ([]dto.RoleResponseDTO, error)
GetRoleByID(roleID string) (*dto.RoleResponseDTO, error) GetRoleByID(ctx context.Context, roleID string) (*dto.RoleResponseDTO, error)
} }
type roleService struct { type roleService struct {
@ -22,8 +23,7 @@ func NewRoleService(roleRepo repositories.RoleRepository) RoleService {
return &roleService{RoleRepo: roleRepo} return &roleService{RoleRepo: roleRepo}
} }
func (s *roleService) GetRoles() ([]dto.RoleResponseDTO, error) { func (s *roleService) GetRoles(ctx context.Context) ([]dto.RoleResponseDTO, error) {
cacheKey := "roles_list" cacheKey := "roles_list"
cachedData, err := utils.GetJSONData(cacheKey) cachedData, err := utils.GetJSONData(cacheKey)
if err == nil && cachedData != nil { if err == nil && cachedData != nil {
@ -44,7 +44,7 @@ func (s *roleService) GetRoles() ([]dto.RoleResponseDTO, error) {
} }
} }
roles, err := s.RoleRepo.FindAll() roles, err := s.RoleRepo.FindAll(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch roles: %v", err) return nil, fmt.Errorf("failed to fetch roles: %v", err)
} }
@ -73,9 +73,9 @@ func (s *roleService) GetRoles() ([]dto.RoleResponseDTO, error) {
return roleDTOs, nil return roleDTOs, nil
} }
func (s *roleService) GetRoleByID(roleID string) (*dto.RoleResponseDTO, error) { func (s *roleService) GetRoleByID(ctx context.Context, roleID string) (*dto.RoleResponseDTO, error) {
role, err := s.RoleRepo.FindByID(roleID) role, err := s.RoleRepo.FindByID(ctx, roleID)
if err != nil { if err != nil {
return nil, fmt.Errorf("role not found: %v", err) return nil, fmt.Errorf("role not found: %v", err)
} }

View File

@ -0,0 +1,75 @@
package trash
import (
"strings"
)
type RequestTrashCategoryDTO struct {
Name string `json:"name"`
EstimatedPrice float64 `json:"estimated_price"`
IconTrash string `json:"icon_trash,omitempty"`
Variety string `json:"variety"`
}
type RequestTrashDetailDTO struct {
CategoryID string `json:"category_id"`
StepOrder int `json:"step"`
IconTrashDetail string `json:"icon_trash_detail,omitempty"`
Description string `json:"description"`
}
type ResponseTrashCategoryDTO struct {
ID string `json:"id,omitempty"`
TrashName string `json:"trash_name,omitempty"`
TrashIcon string `json:"trash_icon,omitempty"`
EstimatedPrice float64 `json:"estimated_price"`
Variety string `json:"variety,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
TrashDetail []ResponseTrashDetailDTO `json:"trash_detail,omitempty"`
}
type ResponseTrashDetailDTO struct {
ID string `json:"trashdetail_id"`
CategoryID string `json:"category_id"`
IconTrashDetail string `json:"trashdetail_icon,omitempty"`
Description string `json:"description"`
StepOrder int `json:"step_order"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func (r *RequestTrashCategoryDTO) ValidateRequestTrashCategoryDTO() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Name) == "" {
errors["name"] = append(errors["name"], "name is required")
}
if r.EstimatedPrice <= 0 {
errors["estimated_price"] = append(errors["estimated_price"], "estimated price must be greater than 0")
}
if strings.TrimSpace(r.Variety) == "" {
errors["variety"] = append(errors["variety"], "variety is required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *RequestTrashDetailDTO) ValidateRequestTrashDetailDTO() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Description) == "" {
errors["description"] = append(errors["description"], "description is required")
}
if strings.TrimSpace(r.CategoryID) == "" {
errors["category_id"] = append(errors["category_id"], "category_id is required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1,521 @@
package trash
import (
"rijig/utils"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
)
type TrashHandler struct {
trashService TrashServiceInterface
}
func NewTrashHandler(trashService TrashServiceInterface) *TrashHandler {
return &TrashHandler{
trashService: trashService,
}
}
func (h *TrashHandler) CreateTrashCategory(c *fiber.Ctx) error {
var req RequestTrashCategoryDTO
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
response, err := h.trashService.CreateTrashCategory(c.Context(), req)
if err != nil {
if strings.Contains(err.Error(), "validation failed") {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error()))
}
return utils.InternalServerError(c, "Failed to create trash category")
}
return utils.CreateSuccessWithData(c, "Trash category created successfully", response)
}
func (h *TrashHandler) CreateTrashCategoryWithIcon(c *fiber.Ctx) error {
var req RequestTrashCategoryDTO
req.Name = c.FormValue("name")
req.Variety = c.FormValue("variety")
if estimatedPriceStr := c.FormValue("estimated_price"); estimatedPriceStr != "" {
if price, err := strconv.ParseFloat(estimatedPriceStr, 64); err == nil {
req.EstimatedPrice = price
}
}
iconFile, err := c.FormFile("icon")
if err != nil && err.Error() != "there is no uploaded file associated with the given key" {
return utils.BadRequest(c, "Invalid icon file")
}
response, err := h.trashService.CreateTrashCategoryWithIcon(c.Context(), req, iconFile)
if err != nil {
if strings.Contains(err.Error(), "validation failed") {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error()))
}
if strings.Contains(err.Error(), "invalid file type") {
return utils.BadRequest(c, err.Error())
}
return utils.InternalServerError(c, "Failed to create trash category")
}
return utils.CreateSuccessWithData(c, "Trash category created successfully", response)
}
func (h *TrashHandler) CreateTrashCategoryWithDetails(c *fiber.Ctx) error {
var req struct {
Category RequestTrashCategoryDTO `json:"category"`
Details []RequestTrashDetailDTO `json:"details"`
}
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
response, err := h.trashService.CreateTrashCategoryWithDetails(c.Context(), req.Category, req.Details)
if err != nil {
if strings.Contains(err.Error(), "validation failed") {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error()))
}
return utils.InternalServerError(c, "Failed to create trash category with details")
}
return utils.CreateSuccessWithData(c, "Trash category with details created successfully", response)
}
func (h *TrashHandler) UpdateTrashCategory(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return utils.BadRequest(c, "Category ID is required")
}
var req RequestTrashCategoryDTO
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
response, err := h.trashService.UpdateTrashCategory(c.Context(), id, req)
if err != nil {
if strings.Contains(err.Error(), "validation failed") {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error()))
}
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Trash category not found")
}
return utils.InternalServerError(c, "Failed to update trash category")
}
return utils.SuccessWithData(c, "Trash category updated successfully", response)
}
func (h *TrashHandler) UpdateTrashCategoryWithIcon(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return utils.BadRequest(c, "Category ID is required")
}
var req RequestTrashCategoryDTO
req.Name = c.FormValue("name")
req.Variety = c.FormValue("variety")
if estimatedPriceStr := c.FormValue("estimated_price"); estimatedPriceStr != "" {
if price, err := strconv.ParseFloat(estimatedPriceStr, 64); err == nil {
req.EstimatedPrice = price
}
}
iconFile, _ := c.FormFile("icon")
response, err := h.trashService.UpdateTrashCategoryWithIcon(c.Context(), id, req, iconFile)
if err != nil {
if strings.Contains(err.Error(), "validation failed") {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error()))
}
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Trash category not found")
}
if strings.Contains(err.Error(), "invalid file type") {
return utils.BadRequest(c, err.Error())
}
return utils.InternalServerError(c, "Failed to update trash category")
}
return utils.SuccessWithData(c, "Trash category updated successfully", response)
}
func (h *TrashHandler) GetAllTrashCategories(c *fiber.Ctx) error {
withDetails := c.Query("with_details", "false")
if withDetails == "true" {
response, err := h.trashService.GetAllTrashCategoriesWithDetails(c.Context())
if err != nil {
return utils.InternalServerError(c, "Failed to get trash categories")
}
return utils.SuccessWithData(c, "Trash categories retrieved successfully", response)
}
response, err := h.trashService.GetAllTrashCategories(c.Context())
if err != nil {
return utils.InternalServerError(c, "Failed to get trash categories")
}
return utils.SuccessWithData(c, "Trash categories retrieved successfully", response)
}
func (h *TrashHandler) GetTrashCategoryByID(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return utils.BadRequest(c, "Category ID is required")
}
withDetails := c.Query("with_details", "false")
if withDetails == "true" {
response, err := h.trashService.GetTrashCategoryByIDWithDetails(c.Context(), id)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Trash category not found")
}
return utils.InternalServerError(c, "Failed to get trash category")
}
return utils.SuccessWithData(c, "Trash category retrieved successfully", response)
}
response, err := h.trashService.GetTrashCategoryByID(c.Context(), id)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Trash category not found")
}
return utils.InternalServerError(c, "Failed to get trash category")
}
return utils.SuccessWithData(c, "Trash category retrieved successfully", response)
}
func (h *TrashHandler) DeleteTrashCategory(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return utils.BadRequest(c, "Category ID is required")
}
err := h.trashService.DeleteTrashCategory(c.Context(), id)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Trash category not found")
}
return utils.InternalServerError(c, "Failed to delete trash category")
}
return utils.Success(c, "Trash category deleted successfully")
}
func (h *TrashHandler) CreateTrashDetail(c *fiber.Ctx) error {
var req RequestTrashDetailDTO
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
response, err := h.trashService.CreateTrashDetail(c.Context(), req)
if err != nil {
if strings.Contains(err.Error(), "validation failed") {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error()))
}
return utils.InternalServerError(c, "Failed to create trash detail")
}
return utils.CreateSuccessWithData(c, "Trash detail created successfully", response)
}
func (h *TrashHandler) CreateTrashDetailWithIcon(c *fiber.Ctx) error {
var req RequestTrashDetailDTO
req.CategoryID = c.FormValue("category_id")
req.Description = c.FormValue("description")
if stepOrderStr := c.FormValue("step_order"); stepOrderStr != "" {
if stepOrder, err := strconv.Atoi(stepOrderStr); err == nil {
req.StepOrder = stepOrder
}
}
iconFile, err := c.FormFile("icon")
if err != nil && err.Error() != "there is no uploaded file associated with the given key" {
return utils.BadRequest(c, "Invalid icon file")
}
response, err := h.trashService.CreateTrashDetailWithIcon(c.Context(), req, iconFile)
if err != nil {
if strings.Contains(err.Error(), "validation failed") {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error()))
}
if strings.Contains(err.Error(), "invalid file type") {
return utils.BadRequest(c, err.Error())
}
return utils.InternalServerError(c, "Failed to create trash detail")
}
return utils.CreateSuccessWithData(c, "Trash detail created successfully", response)
}
func (h *TrashHandler) AddTrashDetailToCategory(c *fiber.Ctx) error {
categoryID := c.Params("categoryId")
if categoryID == "" {
return utils.BadRequest(c, "Category ID is required")
}
var req RequestTrashDetailDTO
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
response, err := h.trashService.AddTrashDetailToCategory(c.Context(), categoryID, req)
if err != nil {
if strings.Contains(err.Error(), "validation failed") {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error()))
}
return utils.InternalServerError(c, "Failed to add trash detail to category")
}
return utils.CreateSuccessWithData(c, "Trash detail added to category successfully", response)
}
func (h *TrashHandler) AddTrashDetailToCategoryWithIcon(c *fiber.Ctx) error {
categoryID := c.Params("categoryId")
if categoryID == "" {
return utils.BadRequest(c, "Category ID is required")
}
var req RequestTrashDetailDTO
req.Description = c.FormValue("description")
if stepOrderStr := c.FormValue("step_order"); stepOrderStr != "" {
if stepOrder, err := strconv.Atoi(stepOrderStr); err == nil {
req.StepOrder = stepOrder
}
}
iconFile, err := c.FormFile("icon")
if err != nil && err.Error() != "there is no uploaded file associated with the given key" {
return utils.BadRequest(c, "Invalid icon file")
}
response, err := h.trashService.AddTrashDetailToCategoryWithIcon(c.Context(), categoryID, req, iconFile)
if err != nil {
if strings.Contains(err.Error(), "validation failed") {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error()))
}
if strings.Contains(err.Error(), "invalid file type") {
return utils.BadRequest(c, err.Error())
}
return utils.InternalServerError(c, "Failed to add trash detail to category")
}
return utils.CreateSuccessWithData(c, "Trash detail added to category successfully", response)
}
func (h *TrashHandler) UpdateTrashDetail(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return utils.BadRequest(c, "Detail ID is required")
}
var req RequestTrashDetailDTO
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
response, err := h.trashService.UpdateTrashDetail(c.Context(), id, req)
if err != nil {
if strings.Contains(err.Error(), "validation failed") {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error()))
}
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Trash detail not found")
}
return utils.InternalServerError(c, "Failed to update trash detail")
}
return utils.SuccessWithData(c, "Trash detail updated successfully", response)
}
func (h *TrashHandler) UpdateTrashDetailWithIcon(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return utils.BadRequest(c, "Detail ID is required")
}
var req RequestTrashDetailDTO
req.Description = c.FormValue("description")
if stepOrderStr := c.FormValue("step_order"); stepOrderStr != "" {
if stepOrder, err := strconv.Atoi(stepOrderStr); err == nil {
req.StepOrder = stepOrder
}
}
iconFile, _ := c.FormFile("icon")
response, err := h.trashService.UpdateTrashDetailWithIcon(c.Context(), id, req, iconFile)
if err != nil {
if strings.Contains(err.Error(), "validation failed") {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error()))
}
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Trash detail not found")
}
if strings.Contains(err.Error(), "invalid file type") {
return utils.BadRequest(c, err.Error())
}
return utils.InternalServerError(c, "Failed to update trash detail")
}
return utils.SuccessWithData(c, "Trash detail updated successfully", response)
}
func (h *TrashHandler) GetTrashDetailsByCategory(c *fiber.Ctx) error {
categoryID := c.Params("categoryId")
if categoryID == "" {
return utils.BadRequest(c, "Category ID is required")
}
response, err := h.trashService.GetTrashDetailsByCategory(c.Context(), categoryID)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Trash category not found")
}
return utils.InternalServerError(c, "Failed to get trash details")
}
return utils.SuccessWithData(c, "Trash details retrieved successfully", response)
}
func (h *TrashHandler) GetTrashDetailByID(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return utils.BadRequest(c, "Detail ID is required")
}
response, err := h.trashService.GetTrashDetailByID(c.Context(), id)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Trash detail not found")
}
return utils.InternalServerError(c, "Failed to get trash detail")
}
return utils.SuccessWithData(c, "Trash detail retrieved successfully", response)
}
func (h *TrashHandler) DeleteTrashDetail(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return utils.BadRequest(c, "Detail ID is required")
}
err := h.trashService.DeleteTrashDetail(c.Context(), id)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Trash detail not found")
}
return utils.InternalServerError(c, "Failed to delete trash detail")
}
return utils.Success(c, "Trash detail deleted successfully")
}
func (h *TrashHandler) BulkCreateTrashDetails(c *fiber.Ctx) error {
categoryID := c.Params("categoryId")
if categoryID == "" {
return utils.BadRequest(c, "Category ID is required")
}
var req struct {
Details []RequestTrashDetailDTO `json:"details"`
}
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
if len(req.Details) == 0 {
return utils.BadRequest(c, "At least one detail is required")
}
response, err := h.trashService.BulkCreateTrashDetails(c.Context(), categoryID, req.Details)
if err != nil {
if strings.Contains(err.Error(), "validation failed") {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error()))
}
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Trash category not found")
}
return utils.InternalServerError(c, "Failed to bulk create trash details")
}
return utils.CreateSuccessWithData(c, "Trash details created successfully", response)
}
func (h *TrashHandler) BulkDeleteTrashDetails(c *fiber.Ctx) error {
var req struct {
DetailIDs []string `json:"detail_ids"`
}
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
if len(req.DetailIDs) == 0 {
return utils.BadRequest(c, "At least one detail ID is required")
}
err := h.trashService.BulkDeleteTrashDetails(c.Context(), req.DetailIDs)
if err != nil {
return utils.InternalServerError(c, "Failed to bulk delete trash details")
}
return utils.Success(c, "Trash details deleted successfully")
}
func (h *TrashHandler) ReorderTrashDetails(c *fiber.Ctx) error {
categoryID := c.Params("categoryId")
if categoryID == "" {
return utils.BadRequest(c, "Category ID is required")
}
var req struct {
OrderedDetailIDs []string `json:"ordered_detail_ids"`
}
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
if len(req.OrderedDetailIDs) == 0 {
return utils.BadRequest(c, "At least one detail ID is required")
}
err := h.trashService.ReorderTrashDetails(c.Context(), categoryID, req.OrderedDetailIDs)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Trash category not found")
}
return utils.InternalServerError(c, "Failed to reorder trash details")
}
return utils.Success(c, "Trash details reordered successfully")
}
func extractValidationErrors(errMsg string) interface{} {
if strings.Contains(errMsg, "validation failed:") {
return strings.TrimSpace(strings.Split(errMsg, "validation failed:")[1])
}
return errMsg
}

View File

@ -0,0 +1,326 @@
package trash
import (
"context"
"errors"
"fmt"
"rijig/model"
"time"
"gorm.io/gorm"
)
type TrashRepositoryInterface interface {
CreateTrashCategory(ctx context.Context, category *model.TrashCategory) error
CreateTrashCategoryWithDetails(ctx context.Context, category *model.TrashCategory, details []model.TrashDetail) error
UpdateTrashCategory(ctx context.Context, id string, updates map[string]interface{}) error
GetAllTrashCategories(ctx context.Context) ([]model.TrashCategory, error)
GetAllTrashCategoriesWithDetails(ctx context.Context) ([]model.TrashCategory, error)
GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error)
GetTrashCategoryByIDWithDetails(ctx context.Context, id string) (*model.TrashCategory, error)
DeleteTrashCategory(ctx context.Context, id string) error
CreateTrashDetail(ctx context.Context, detail *model.TrashDetail) error
AddTrashDetailToCategory(ctx context.Context, categoryID string, detail *model.TrashDetail) error
UpdateTrashDetail(ctx context.Context, id string, updates map[string]interface{}) error
GetTrashDetailsByCategory(ctx context.Context, categoryID string) ([]model.TrashDetail, error)
GetTrashDetailByID(ctx context.Context, id string) (*model.TrashDetail, error)
DeleteTrashDetail(ctx context.Context, id string) error
CheckTrashCategoryExists(ctx context.Context, id string) (bool, error)
CheckTrashDetailExists(ctx context.Context, id string) (bool, error)
GetMaxStepOrderByCategory(ctx context.Context, categoryID string) (int, error)
}
type TrashRepository struct {
db *gorm.DB
}
func NewTrashRepository(db *gorm.DB) TrashRepositoryInterface {
return &TrashRepository{
db: db,
}
}
func (r *TrashRepository) CreateTrashCategory(ctx context.Context, category *model.TrashCategory) error {
if err := r.db.WithContext(ctx).Create(category).Error; err != nil {
return fmt.Errorf("failed to create trash category: %w", err)
}
return nil
}
func (r *TrashRepository) CreateTrashCategoryWithDetails(ctx context.Context, category *model.TrashCategory, details []model.TrashDetail) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(category).Error; err != nil {
return fmt.Errorf("failed to create trash category: %w", err)
}
if len(details) > 0 {
for i := range details {
details[i].TrashCategoryID = category.ID
if details[i].StepOrder == 0 {
details[i].StepOrder = i + 1
}
}
if err := tx.Create(&details).Error; err != nil {
return fmt.Errorf("failed to create trash details: %w", err)
}
}
return nil
})
}
func (r *TrashRepository) UpdateTrashCategory(ctx context.Context, id string, updates map[string]interface{}) error {
exists, err := r.CheckTrashCategoryExists(ctx, id)
if err != nil {
return err
}
if !exists {
return errors.New("trash category not found")
}
updates["updated_at"] = time.Now()
result := r.db.WithContext(ctx).Model(&model.TrashCategory{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return fmt.Errorf("failed to update trash category: %w", result.Error)
}
if result.RowsAffected == 0 {
return errors.New("no rows affected during update")
}
return nil
}
func (r *TrashRepository) GetAllTrashCategories(ctx context.Context) ([]model.TrashCategory, error) {
var categories []model.TrashCategory
if err := r.db.WithContext(ctx).Find(&categories).Error; err != nil {
return nil, fmt.Errorf("failed to get trash categories: %w", err)
}
return categories, nil
}
func (r *TrashRepository) GetAllTrashCategoriesWithDetails(ctx context.Context) ([]model.TrashCategory, error) {
var categories []model.TrashCategory
if err := r.db.WithContext(ctx).Preload("Details", func(db *gorm.DB) *gorm.DB {
return db.Order("step_order ASC")
}).Find(&categories).Error; err != nil {
return nil, fmt.Errorf("failed to get trash categories with details: %w", err)
}
return categories, nil
}
func (r *TrashRepository) GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) {
var category model.TrashCategory
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&category).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("trash category not found")
}
return nil, fmt.Errorf("failed to get trash category: %w", err)
}
return &category, nil
}
func (r *TrashRepository) GetTrashCategoryByIDWithDetails(ctx context.Context, id string) (*model.TrashCategory, error) {
var category model.TrashCategory
if err := r.db.WithContext(ctx).Preload("Details", func(db *gorm.DB) *gorm.DB {
return db.Order("step_order ASC")
}).Where("id = ?", id).First(&category).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("trash category not found")
}
return nil, fmt.Errorf("failed to get trash category with details: %w", err)
}
return &category, nil
}
func (r *TrashRepository) DeleteTrashCategory(ctx context.Context, id string) error {
exists, err := r.CheckTrashCategoryExists(ctx, id)
if err != nil {
return err
}
if !exists {
return errors.New("trash category not found")
}
result := r.db.WithContext(ctx).Delete(&model.TrashCategory{ID: id})
if result.Error != nil {
return fmt.Errorf("failed to delete trash category: %w", result.Error)
}
if result.RowsAffected == 0 {
return errors.New("no rows affected during deletion")
}
return nil
}
func (r *TrashRepository) CreateTrashDetail(ctx context.Context, detail *model.TrashDetail) error {
exists, err := r.CheckTrashCategoryExists(ctx, detail.TrashCategoryID)
if err != nil {
return err
}
if !exists {
return errors.New("trash category not found")
}
if detail.StepOrder == 0 {
maxOrder, err := r.GetMaxStepOrderByCategory(ctx, detail.TrashCategoryID)
if err != nil {
return err
}
detail.StepOrder = maxOrder + 1
}
if err := r.db.WithContext(ctx).Create(detail).Error; err != nil {
return fmt.Errorf("failed to create trash detail: %w", err)
}
return nil
}
func (r *TrashRepository) AddTrashDetailToCategory(ctx context.Context, categoryID string, detail *model.TrashDetail) error {
exists, err := r.CheckTrashCategoryExists(ctx, categoryID)
if err != nil {
return err
}
if !exists {
return errors.New("trash category not found")
}
detail.TrashCategoryID = categoryID
if detail.StepOrder == 0 {
maxOrder, err := r.GetMaxStepOrderByCategory(ctx, categoryID)
if err != nil {
return err
}
detail.StepOrder = maxOrder + 1
}
if err := r.db.WithContext(ctx).Create(detail).Error; err != nil {
return fmt.Errorf("failed to add trash detail to category: %w", err)
}
return nil
}
func (r *TrashRepository) UpdateTrashDetail(ctx context.Context, id string, updates map[string]interface{}) error {
exists, err := r.CheckTrashDetailExists(ctx, id)
if err != nil {
return err
}
if !exists {
return errors.New("trash detail not found")
}
updates["updated_at"] = time.Now()
result := r.db.WithContext(ctx).Model(&model.TrashDetail{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return fmt.Errorf("failed to update trash detail: %w", result.Error)
}
if result.RowsAffected == 0 {
return errors.New("no rows affected during update")
}
return nil
}
func (r *TrashRepository) GetTrashDetailsByCategory(ctx context.Context, categoryID string) ([]model.TrashDetail, error) {
var details []model.TrashDetail
if err := r.db.WithContext(ctx).Where("trash_category_id = ?", categoryID).Order("step_order ASC").Find(&details).Error; err != nil {
return nil, fmt.Errorf("failed to get trash details: %w", err)
}
return details, nil
}
func (r *TrashRepository) GetTrashDetailByID(ctx context.Context, id string) (*model.TrashDetail, error) {
var detail model.TrashDetail
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&detail).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("trash detail not found")
}
return nil, fmt.Errorf("failed to get trash detail: %w", err)
}
return &detail, nil
}
func (r *TrashRepository) DeleteTrashDetail(ctx context.Context, id string) error {
exists, err := r.CheckTrashDetailExists(ctx, id)
if err != nil {
return err
}
if !exists {
return errors.New("trash detail not found")
}
result := r.db.WithContext(ctx).Delete(&model.TrashDetail{ID: id})
if result.Error != nil {
return fmt.Errorf("failed to delete trash detail: %w", result.Error)
}
if result.RowsAffected == 0 {
return errors.New("no rows affected during deletion")
}
return nil
}
func (r *TrashRepository) CheckTrashCategoryExists(ctx context.Context, id string) (bool, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&model.TrashCategory{}).Where("id = ?", id).Count(&count).Error; err != nil {
return false, fmt.Errorf("failed to check trash category existence: %w", err)
}
return count > 0, nil
}
func (r *TrashRepository) CheckTrashDetailExists(ctx context.Context, id string) (bool, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&model.TrashDetail{}).Where("id = ?", id).Count(&count).Error; err != nil {
return false, fmt.Errorf("failed to check trash detail existence: %w", err)
}
return count > 0, nil
}
func (r *TrashRepository) GetMaxStepOrderByCategory(ctx context.Context, categoryID string) (int, error) {
var maxOrder int
if err := r.db.WithContext(ctx).Model(&model.TrashDetail{}).
Where("trash_category_id = ?", categoryID).
Select("COALESCE(MAX(step_order), 0)").
Scan(&maxOrder).Error; err != nil {
return 0, fmt.Errorf("failed to get max step order: %w", err)
}
return maxOrder, nil
}

View File

@ -0,0 +1 @@
package trash

View File

@ -0,0 +1,750 @@
package trash
import (
"context"
"errors"
"fmt"
"log"
"mime/multipart"
"os"
"path/filepath"
"rijig/model"
"strings"
"time"
"github.com/google/uuid"
)
type TrashServiceInterface interface {
CreateTrashCategory(ctx context.Context, req RequestTrashCategoryDTO) (*ResponseTrashCategoryDTO, error)
CreateTrashCategoryWithDetails(ctx context.Context, categoryReq RequestTrashCategoryDTO, detailsReq []RequestTrashDetailDTO) (*ResponseTrashCategoryDTO, error)
CreateTrashCategoryWithIcon(ctx context.Context, req RequestTrashCategoryDTO, iconFile *multipart.FileHeader) (*ResponseTrashCategoryDTO, error)
UpdateTrashCategory(ctx context.Context, id string, req RequestTrashCategoryDTO) (*ResponseTrashCategoryDTO, error)
UpdateTrashCategoryWithIcon(ctx context.Context, id string, req RequestTrashCategoryDTO, iconFile *multipart.FileHeader) (*ResponseTrashCategoryDTO, error)
GetAllTrashCategories(ctx context.Context) ([]ResponseTrashCategoryDTO, error)
GetAllTrashCategoriesWithDetails(ctx context.Context) ([]ResponseTrashCategoryDTO, error)
GetTrashCategoryByID(ctx context.Context, id string) (*ResponseTrashCategoryDTO, error)
GetTrashCategoryByIDWithDetails(ctx context.Context, id string) (*ResponseTrashCategoryDTO, error)
DeleteTrashCategory(ctx context.Context, id string) error
CreateTrashDetail(ctx context.Context, req RequestTrashDetailDTO) (*ResponseTrashDetailDTO, error)
CreateTrashDetailWithIcon(ctx context.Context, req RequestTrashDetailDTO, iconFile *multipart.FileHeader) (*ResponseTrashDetailDTO, error)
AddTrashDetailToCategory(ctx context.Context, categoryID string, req RequestTrashDetailDTO) (*ResponseTrashDetailDTO, error)
AddTrashDetailToCategoryWithIcon(ctx context.Context, categoryID string, req RequestTrashDetailDTO, iconFile *multipart.FileHeader) (*ResponseTrashDetailDTO, error)
UpdateTrashDetail(ctx context.Context, id string, req RequestTrashDetailDTO) (*ResponseTrashDetailDTO, error)
UpdateTrashDetailWithIcon(ctx context.Context, id string, req RequestTrashDetailDTO, iconFile *multipart.FileHeader) (*ResponseTrashDetailDTO, error)
GetTrashDetailsByCategory(ctx context.Context, categoryID string) ([]ResponseTrashDetailDTO, error)
GetTrashDetailByID(ctx context.Context, id string) (*ResponseTrashDetailDTO, error)
DeleteTrashDetail(ctx context.Context, id string) error
BulkCreateTrashDetails(ctx context.Context, categoryID string, detailsReq []RequestTrashDetailDTO) ([]ResponseTrashDetailDTO, error)
BulkDeleteTrashDetails(ctx context.Context, detailIDs []string) error
ReorderTrashDetails(ctx context.Context, categoryID string, orderedDetailIDs []string) error
}
type TrashService struct {
trashRepo TrashRepositoryInterface
}
func NewTrashService(trashRepo TrashRepositoryInterface) TrashServiceInterface {
return &TrashService{
trashRepo: trashRepo,
}
}
func (s *TrashService) saveIconOfTrash(iconTrash *multipart.FileHeader) (string, error) {
pathImage := "/uploads/icontrash/"
iconTrashDir := "./public" + os.Getenv("BASE_URL") + pathImage
if _, err := os.Stat(iconTrashDir); os.IsNotExist(err) {
if err := os.MkdirAll(iconTrashDir, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to create directory for icon trash: %v", err)
}
}
allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true}
extension := strings.ToLower(filepath.Ext(iconTrash.Filename))
if !allowedExtensions[extension] {
return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, .png, and .svg are allowed")
}
iconTrashFileName := fmt.Sprintf("%s_icontrash%s", uuid.New().String(), extension)
iconTrashPath := filepath.Join(iconTrashDir, iconTrashFileName)
src, err := iconTrash.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %v", err)
}
defer src.Close()
dst, err := os.Create(iconTrashPath)
if err != nil {
return "", fmt.Errorf("failed to create icon trash file: %v", err)
}
defer dst.Close()
if _, err := dst.ReadFrom(src); err != nil {
return "", fmt.Errorf("failed to save icon trash: %v", err)
}
iconTrashUrl := fmt.Sprintf("%s%s", pathImage, iconTrashFileName)
return iconTrashUrl, nil
}
func (s *TrashService) saveIconOfTrashDetail(iconTrashDetail *multipart.FileHeader) (string, error) {
pathImage := "/uploads/icontrashdetail/"
iconTrashDetailDir := "./public" + os.Getenv("BASE_URL") + pathImage
if _, err := os.Stat(iconTrashDetailDir); os.IsNotExist(err) {
if err := os.MkdirAll(iconTrashDetailDir, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to create directory for icon trash detail: %v", err)
}
}
allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true}
extension := strings.ToLower(filepath.Ext(iconTrashDetail.Filename))
if !allowedExtensions[extension] {
return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, .png, and .svg are allowed")
}
iconTrashDetailFileName := fmt.Sprintf("%s_icontrashdetail%s", uuid.New().String(), extension)
iconTrashDetailPath := filepath.Join(iconTrashDetailDir, iconTrashDetailFileName)
src, err := iconTrashDetail.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %v", err)
}
defer src.Close()
dst, err := os.Create(iconTrashDetailPath)
if err != nil {
return "", fmt.Errorf("failed to create icon trash detail file: %v", err)
}
defer dst.Close()
if _, err := dst.ReadFrom(src); err != nil {
return "", fmt.Errorf("failed to save icon trash detail: %v", err)
}
iconTrashDetailUrl := fmt.Sprintf("%s%s", pathImage, iconTrashDetailFileName)
return iconTrashDetailUrl, nil
}
func (s *TrashService) deleteIconTrashFile(imagePath string) error {
if imagePath == "" {
return nil
}
baseDir := "./public/" + os.Getenv("BASE_URL")
absolutePath := baseDir + imagePath
if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
return fmt.Errorf("image file not found: %v", err)
}
err := os.Remove(absolutePath)
if err != nil {
return fmt.Errorf("failed to delete image: %v", err)
}
log.Printf("Image deleted successfully: %s", absolutePath)
return nil
}
func (s *TrashService) deleteIconTrashDetailFile(imagePath string) error {
if imagePath == "" {
return nil
}
baseDir := "./public/" + os.Getenv("BASE_URL")
absolutePath := baseDir + imagePath
if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
return fmt.Errorf("image file not found: %v", err)
}
err := os.Remove(absolutePath)
if err != nil {
return fmt.Errorf("failed to delete image: %v", err)
}
log.Printf("Trash detail image deleted successfully: %s", absolutePath)
return nil
}
func (s *TrashService) CreateTrashCategoryWithIcon(ctx context.Context, req RequestTrashCategoryDTO, iconFile *multipart.FileHeader) (*ResponseTrashCategoryDTO, error) {
if errors, valid := req.ValidateRequestTrashCategoryDTO(); !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
var iconUrl string
var err error
if iconFile != nil {
iconUrl, err = s.saveIconOfTrash(iconFile)
if err != nil {
return nil, fmt.Errorf("failed to save icon: %w", err)
}
}
category := &model.TrashCategory{
Name: req.Name,
IconTrash: iconUrl,
EstimatedPrice: req.EstimatedPrice,
Variety: req.Variety,
}
if err := s.trashRepo.CreateTrashCategory(ctx, category); err != nil {
if iconUrl != "" {
s.deleteIconTrashFile(iconUrl)
}
return nil, fmt.Errorf("failed to create trash category: %w", err)
}
response := s.convertTrashCategoryToResponseDTO(category)
return response, nil
}
func (s *TrashService) UpdateTrashCategoryWithIcon(ctx context.Context, id string, req RequestTrashCategoryDTO, iconFile *multipart.FileHeader) (*ResponseTrashCategoryDTO, error) {
if errors, valid := req.ValidateRequestTrashCategoryDTO(); !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
existingCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get existing category: %w", err)
}
var iconUrl string = existingCategory.IconTrash
if iconFile != nil {
newIconUrl, err := s.saveIconOfTrash(iconFile)
if err != nil {
return nil, fmt.Errorf("failed to save new icon: %w", err)
}
iconUrl = newIconUrl
}
updates := map[string]interface{}{
"name": req.Name,
"icon_trash": iconUrl,
"estimated_price": req.EstimatedPrice,
"variety": req.Variety,
}
if err := s.trashRepo.UpdateTrashCategory(ctx, id, updates); err != nil {
if iconFile != nil && iconUrl != existingCategory.IconTrash {
s.deleteIconTrashFile(iconUrl)
}
return nil, fmt.Errorf("failed to update trash category: %w", err)
}
if iconFile != nil && existingCategory.IconTrash != "" && iconUrl != existingCategory.IconTrash {
if err := s.deleteIconTrashFile(existingCategory.IconTrash); err != nil {
log.Printf("Warning: failed to delete old icon: %v", err)
}
}
updatedCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to retrieve updated category: %w", err)
}
response := s.convertTrashCategoryToResponseDTO(updatedCategory)
return response, nil
}
func (s *TrashService) CreateTrashDetailWithIcon(ctx context.Context, req RequestTrashDetailDTO, iconFile *multipart.FileHeader) (*ResponseTrashDetailDTO, error) {
if errors, valid := req.ValidateRequestTrashDetailDTO(); !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
var iconUrl string
var err error
if iconFile != nil {
iconUrl, err = s.saveIconOfTrashDetail(iconFile)
if err != nil {
return nil, fmt.Errorf("failed to save icon: %w", err)
}
}
detail := &model.TrashDetail{
TrashCategoryID: req.CategoryID,
IconTrashDetail: iconUrl,
Description: req.Description,
StepOrder: req.StepOrder,
}
if err := s.trashRepo.CreateTrashDetail(ctx, detail); err != nil {
if iconUrl != "" {
s.deleteIconTrashDetailFile(iconUrl)
}
return nil, fmt.Errorf("failed to create trash detail: %w", err)
}
response := s.convertTrashDetailToResponseDTO(detail)
return response, nil
}
func (s *TrashService) AddTrashDetailToCategoryWithIcon(ctx context.Context, categoryID string, req RequestTrashDetailDTO, iconFile *multipart.FileHeader) (*ResponseTrashDetailDTO, error) {
if errors, valid := req.ValidateRequestTrashDetailDTO(); !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
var iconUrl string
var err error
if iconFile != nil {
iconUrl, err = s.saveIconOfTrashDetail(iconFile)
if err != nil {
return nil, fmt.Errorf("failed to save icon: %w", err)
}
}
detail := &model.TrashDetail{
IconTrashDetail: iconUrl,
Description: req.Description,
StepOrder: req.StepOrder,
}
if err := s.trashRepo.AddTrashDetailToCategory(ctx, categoryID, detail); err != nil {
if iconUrl != "" {
s.deleteIconTrashDetailFile(iconUrl)
}
return nil, fmt.Errorf("failed to add trash detail to category: %w", err)
}
response := s.convertTrashDetailToResponseDTO(detail)
return response, nil
}
func (s *TrashService) UpdateTrashDetailWithIcon(ctx context.Context, id string, req RequestTrashDetailDTO, iconFile *multipart.FileHeader) (*ResponseTrashDetailDTO, error) {
if errors, valid := req.ValidateRequestTrashDetailDTO(); !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
existingDetail, err := s.trashRepo.GetTrashDetailByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get existing detail: %w", err)
}
var iconUrl string = existingDetail.IconTrashDetail
if iconFile != nil {
newIconUrl, err := s.saveIconOfTrashDetail(iconFile)
if err != nil {
return nil, fmt.Errorf("failed to save new icon: %w", err)
}
iconUrl = newIconUrl
}
updates := map[string]interface{}{
"icon_trash_detail": iconUrl,
"description": req.Description,
"step_order": req.StepOrder,
}
if err := s.trashRepo.UpdateTrashDetail(ctx, id, updates); err != nil {
if iconFile != nil && iconUrl != existingDetail.IconTrashDetail {
s.deleteIconTrashDetailFile(iconUrl)
}
return nil, fmt.Errorf("failed to update trash detail: %w", err)
}
if iconFile != nil && existingDetail.IconTrashDetail != "" && iconUrl != existingDetail.IconTrashDetail {
if err := s.deleteIconTrashDetailFile(existingDetail.IconTrashDetail); err != nil {
log.Printf("Warning: failed to delete old icon: %v", err)
}
}
updatedDetail, err := s.trashRepo.GetTrashDetailByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to retrieve updated detail: %w", err)
}
response := s.convertTrashDetailToResponseDTO(updatedDetail)
return response, nil
}
func (s *TrashService) CreateTrashCategory(ctx context.Context, req RequestTrashCategoryDTO) (*ResponseTrashCategoryDTO, error) {
if errors, valid := req.ValidateRequestTrashCategoryDTO(); !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
category := &model.TrashCategory{
Name: req.Name,
IconTrash: req.IconTrash,
EstimatedPrice: req.EstimatedPrice,
Variety: req.Variety,
}
if err := s.trashRepo.CreateTrashCategory(ctx, category); err != nil {
return nil, fmt.Errorf("failed to create trash category: %w", err)
}
response := s.convertTrashCategoryToResponseDTO(category)
return response, nil
}
func (s *TrashService) CreateTrashCategoryWithDetails(ctx context.Context, categoryReq RequestTrashCategoryDTO, detailsReq []RequestTrashDetailDTO) (*ResponseTrashCategoryDTO, error) {
if errors, valid := categoryReq.ValidateRequestTrashCategoryDTO(); !valid {
return nil, fmt.Errorf("category validation failed: %v", errors)
}
for i, detailReq := range detailsReq {
if errors, valid := detailReq.ValidateRequestTrashDetailDTO(); !valid {
return nil, fmt.Errorf("detail %d validation failed: %v", i+1, errors)
}
}
category := &model.TrashCategory{
Name: categoryReq.Name,
IconTrash: categoryReq.IconTrash,
EstimatedPrice: categoryReq.EstimatedPrice,
Variety: categoryReq.Variety,
}
details := make([]model.TrashDetail, len(detailsReq))
for i, detailReq := range detailsReq {
details[i] = model.TrashDetail{
IconTrashDetail: detailReq.IconTrashDetail,
Description: detailReq.Description,
StepOrder: detailReq.StepOrder,
}
}
if err := s.trashRepo.CreateTrashCategoryWithDetails(ctx, category, details); err != nil {
return nil, fmt.Errorf("failed to create trash category with details: %w", err)
}
createdCategory, err := s.trashRepo.GetTrashCategoryByIDWithDetails(ctx, category.ID)
if err != nil {
return nil, fmt.Errorf("failed to retrieve created category: %w", err)
}
response := s.convertTrashCategoryToResponseDTOWithDetails(createdCategory)
return response, nil
}
func (s *TrashService) UpdateTrashCategory(ctx context.Context, id string, req RequestTrashCategoryDTO) (*ResponseTrashCategoryDTO, error) {
if errors, valid := req.ValidateRequestTrashCategoryDTO(); !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
exists, err := s.trashRepo.CheckTrashCategoryExists(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to check category existence: %w", err)
}
if !exists {
return nil, errors.New("trash category not found")
}
updates := map[string]interface{}{
"name": req.Name,
"icon_trash": req.IconTrash,
"estimated_price": req.EstimatedPrice,
"variety": req.Variety,
}
if err := s.trashRepo.UpdateTrashCategory(ctx, id, updates); err != nil {
return nil, fmt.Errorf("failed to update trash category: %w", err)
}
updatedCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to retrieve updated category: %w", err)
}
response := s.convertTrashCategoryToResponseDTO(updatedCategory)
return response, nil
}
func (s *TrashService) GetAllTrashCategories(ctx context.Context) ([]ResponseTrashCategoryDTO, error) {
categories, err := s.trashRepo.GetAllTrashCategories(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get trash categories: %w", err)
}
responses := make([]ResponseTrashCategoryDTO, len(categories))
for i, category := range categories {
responses[i] = *s.convertTrashCategoryToResponseDTO(&category)
}
return responses, nil
}
func (s *TrashService) GetAllTrashCategoriesWithDetails(ctx context.Context) ([]ResponseTrashCategoryDTO, error) {
categories, err := s.trashRepo.GetAllTrashCategoriesWithDetails(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get trash categories with details: %w", err)
}
responses := make([]ResponseTrashCategoryDTO, len(categories))
for i, category := range categories {
responses[i] = *s.convertTrashCategoryToResponseDTOWithDetails(&category)
}
return responses, nil
}
func (s *TrashService) GetTrashCategoryByID(ctx context.Context, id string) (*ResponseTrashCategoryDTO, error) {
category, err := s.trashRepo.GetTrashCategoryByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get trash category: %w", err)
}
response := s.convertTrashCategoryToResponseDTO(category)
return response, nil
}
func (s *TrashService) GetTrashCategoryByIDWithDetails(ctx context.Context, id string) (*ResponseTrashCategoryDTO, error) {
category, err := s.trashRepo.GetTrashCategoryByIDWithDetails(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get trash category with details: %w", err)
}
response := s.convertTrashCategoryToResponseDTOWithDetails(category)
return response, nil
}
func (s *TrashService) DeleteTrashCategory(ctx context.Context, id string) error {
category, err := s.trashRepo.GetTrashCategoryByID(ctx, id)
if err != nil {
return fmt.Errorf("failed to get category: %w", err)
}
if err := s.trashRepo.DeleteTrashCategory(ctx, id); err != nil {
return fmt.Errorf("failed to delete trash category: %w", err)
}
if category.IconTrash != "" {
if err := s.deleteIconTrashFile(category.IconTrash); err != nil {
log.Printf("Warning: failed to delete category icon: %v", err)
}
}
return nil
}
func (s *TrashService) CreateTrashDetail(ctx context.Context, req RequestTrashDetailDTO) (*ResponseTrashDetailDTO, error) {
if errors, valid := req.ValidateRequestTrashDetailDTO(); !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
detail := &model.TrashDetail{
TrashCategoryID: req.CategoryID,
IconTrashDetail: req.IconTrashDetail,
Description: req.Description,
StepOrder: req.StepOrder,
}
if err := s.trashRepo.CreateTrashDetail(ctx, detail); err != nil {
return nil, fmt.Errorf("failed to create trash detail: %w", err)
}
response := s.convertTrashDetailToResponseDTO(detail)
return response, nil
}
func (s *TrashService) AddTrashDetailToCategory(ctx context.Context, categoryID string, req RequestTrashDetailDTO) (*ResponseTrashDetailDTO, error) {
if errors, valid := req.ValidateRequestTrashDetailDTO(); !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
detail := &model.TrashDetail{
IconTrashDetail: req.IconTrashDetail,
Description: req.Description,
StepOrder: req.StepOrder,
}
if err := s.trashRepo.AddTrashDetailToCategory(ctx, categoryID, detail); err != nil {
return nil, fmt.Errorf("failed to add trash detail to category: %w", err)
}
response := s.convertTrashDetailToResponseDTO(detail)
return response, nil
}
func (s *TrashService) UpdateTrashDetail(ctx context.Context, id string, req RequestTrashDetailDTO) (*ResponseTrashDetailDTO, error) {
if errors, valid := req.ValidateRequestTrashDetailDTO(); !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
exists, err := s.trashRepo.CheckTrashDetailExists(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to check detail existence: %w", err)
}
if !exists {
return nil, errors.New("trash detail not found")
}
updates := map[string]interface{}{
"icon_trash_detail": req.IconTrashDetail,
"description": req.Description,
"step_order": req.StepOrder,
}
if err := s.trashRepo.UpdateTrashDetail(ctx, id, updates); err != nil {
return nil, fmt.Errorf("failed to update trash detail: %w", err)
}
updatedDetail, err := s.trashRepo.GetTrashDetailByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to retrieve updated detail: %w", err)
}
response := s.convertTrashDetailToResponseDTO(updatedDetail)
return response, nil
}
func (s *TrashService) GetTrashDetailsByCategory(ctx context.Context, categoryID string) ([]ResponseTrashDetailDTO, error) {
exists, err := s.trashRepo.CheckTrashCategoryExists(ctx, categoryID)
if err != nil {
return nil, fmt.Errorf("failed to check category existence: %w", err)
}
if !exists {
return nil, errors.New("trash category not found")
}
details, err := s.trashRepo.GetTrashDetailsByCategory(ctx, categoryID)
if err != nil {
return nil, fmt.Errorf("failed to get trash details: %w", err)
}
responses := make([]ResponseTrashDetailDTO, len(details))
for i, detail := range details {
responses[i] = *s.convertTrashDetailToResponseDTO(&detail)
}
return responses, nil
}
func (s *TrashService) GetTrashDetailByID(ctx context.Context, id string) (*ResponseTrashDetailDTO, error) {
detail, err := s.trashRepo.GetTrashDetailByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get trash detail: %w", err)
}
response := s.convertTrashDetailToResponseDTO(detail)
return response, nil
}
func (s *TrashService) DeleteTrashDetail(ctx context.Context, id string) error {
detail, err := s.trashRepo.GetTrashDetailByID(ctx, id)
if err != nil {
return fmt.Errorf("failed to get detail: %w", err)
}
if err := s.trashRepo.DeleteTrashDetail(ctx, id); err != nil {
return fmt.Errorf("failed to delete trash detail: %w", err)
}
if detail.IconTrashDetail != "" {
if err := s.deleteIconTrashDetailFile(detail.IconTrashDetail); err != nil {
log.Printf("Warning: failed to delete detail icon: %v", err)
}
}
return nil
}
func (s *TrashService) BulkCreateTrashDetails(ctx context.Context, categoryID string, detailsReq []RequestTrashDetailDTO) ([]ResponseTrashDetailDTO, error) {
exists, err := s.trashRepo.CheckTrashCategoryExists(ctx, categoryID)
if err != nil {
return nil, fmt.Errorf("failed to check category existence: %w", err)
}
if !exists {
return nil, errors.New("trash category not found")
}
for i, detailReq := range detailsReq {
if errors, valid := detailReq.ValidateRequestTrashDetailDTO(); !valid {
return nil, fmt.Errorf("detail %d validation failed: %v", i+1, errors)
}
}
responses := make([]ResponseTrashDetailDTO, len(detailsReq))
for i, detailReq := range detailsReq {
response, err := s.AddTrashDetailToCategory(ctx, categoryID, detailReq)
if err != nil {
return nil, fmt.Errorf("failed to create detail %d: %w", i+1, err)
}
responses[i] = *response
}
return responses, nil
}
func (s *TrashService) BulkDeleteTrashDetails(ctx context.Context, detailIDs []string) error {
for _, id := range detailIDs {
if err := s.DeleteTrashDetail(ctx, id); err != nil {
return fmt.Errorf("failed to delete detail %s: %w", id, err)
}
}
return nil
}
func (s *TrashService) ReorderTrashDetails(ctx context.Context, categoryID string, orderedDetailIDs []string) error {
exists, err := s.trashRepo.CheckTrashCategoryExists(ctx, categoryID)
if err != nil {
return fmt.Errorf("failed to check category existence: %w", err)
}
if !exists {
return errors.New("trash category not found")
}
for i, detailID := range orderedDetailIDs {
updates := map[string]interface{}{
"step_order": i + 1,
}
if err := s.trashRepo.UpdateTrashDetail(ctx, detailID, updates); err != nil {
return fmt.Errorf("failed to reorder detail %s: %w", detailID, err)
}
}
return nil
}
func (s *TrashService) convertTrashCategoryToResponseDTO(category *model.TrashCategory) *ResponseTrashCategoryDTO {
return &ResponseTrashCategoryDTO{
ID: category.ID,
TrashName: category.Name,
TrashIcon: category.IconTrash,
EstimatedPrice: category.EstimatedPrice,
Variety: category.Variety,
CreatedAt: category.CreatedAt.Format(time.RFC3339),
UpdatedAt: category.UpdatedAt.Format(time.RFC3339),
}
}
func (s *TrashService) convertTrashCategoryToResponseDTOWithDetails(category *model.TrashCategory) *ResponseTrashCategoryDTO {
response := s.convertTrashCategoryToResponseDTO(category)
details := make([]ResponseTrashDetailDTO, len(category.Details))
for i, detail := range category.Details {
details[i] = *s.convertTrashDetailToResponseDTO(&detail)
}
response.TrashDetail = details
return response
}
func (s *TrashService) convertTrashDetailToResponseDTO(detail *model.TrashDetail) *ResponseTrashDetailDTO {
return &ResponseTrashDetailDTO{
ID: detail.ID,
CategoryID: detail.TrashCategoryID,
IconTrashDetail: detail.IconTrashDetail,
Description: detail.Description,
StepOrder: detail.StepOrder,
CreatedAt: detail.CreatedAt.Format(time.RFC3339),
UpdatedAt: detail.UpdatedAt.Format(time.RFC3339),
}
}

View File

@ -0,0 +1,48 @@
package userpin
import (
"rijig/utils"
"strings"
)
type RequestPinDTO struct {
DeviceId string `json:"device_id"`
Pin string `json:"userpin"`
}
func (r *RequestPinDTO) ValidateRequestPinDTO() (map[string][]string, bool) {
errors := make(map[string][]string)
if err := utils.ValidatePin(r.Pin); err != nil {
errors["pin"] = append(errors["pin"], err.Error())
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
type UpdatePinDTO struct {
OldPin string `json:"old_pin"`
NewPin string `json:"new_pin"`
}
func (u *UpdatePinDTO) ValidateUpdatePinDTO() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(u.OldPin) == "" {
errors["old_pin"] = append(errors["old_pin"], "Old pin is required")
}
if err := utils.ValidatePin(u.NewPin); err != nil {
errors["new_pin"] = append(errors["new_pin"], err.Error())
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1,77 @@
package userpin
import (
"rijig/middleware"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
type UserPinHandler struct {
service UserPinService
}
func NewUserPinHandler(service UserPinService) *UserPinHandler {
return &UserPinHandler{service}
}
// userID, ok := c.Locals("user_id").(string)
//
// if !ok || userID == "" {
// return utils.Unauthorized(c, "user_id is missing or invalid")
// }
func (h *UserPinHandler) CreateUserPinHandler(c *fiber.Ctx) error {
// Ambil klaim pengguna yang sudah diautentikasi
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
// Parsing body request untuk PIN
var req RequestPinDTO
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
// Validasi request PIN
if errs, ok := req.ValidateRequestPinDTO(); !ok {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation error", errs)
}
// Panggil service untuk membuat PIN
err = h.service.CreateUserPin(c.Context(), claims.UserID, &req)
if err != nil {
if err.Error() == "PIN already created" {
return utils.BadRequest(c, err.Error()) // Jika PIN sudah ada, kembalikan error 400
}
return utils.InternalServerError(c, err.Error()) // Jika terjadi error lain, internal server error
}
// Mengembalikan response sukses jika berhasil
return utils.Success(c, "PIN created successfully")
}
func (h *UserPinHandler) VerifyPinHandler(c *fiber.Ctx) error {
// userID, ok := c.Locals("user_id").(string)
// if !ok || userID == "" {
// return utils.Unauthorized(c, "user_id is missing or invalid")
// }
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
var req RequestPinDTO
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
token, err := h.service.VerifyUserPin(c.Context(), claims.UserID, &req)
if err != nil {
return utils.BadRequest(c, err.Error())
}
return utils.SuccessWithData(c, "PIN verified successfully", fiber.Map{
"token": token,
})
}

View File

@ -0,0 +1,50 @@
package userpin
import (
"context"
"rijig/model"
"gorm.io/gorm"
)
type UserPinRepository interface {
FindByUserID(ctx context.Context, userID string) (*model.UserPin, error)
Create(ctx context.Context, userPin *model.UserPin) error
Update(ctx context.Context, userPin *model.UserPin) error
}
type userPinRepository struct {
db *gorm.DB
}
func NewUserPinRepository(db *gorm.DB) UserPinRepository {
return &userPinRepository{db}
}
func (r *userPinRepository) FindByUserID(ctx context.Context, userID string) (*model.UserPin, error) {
var userPin model.UserPin
err := r.db.WithContext(ctx).Where("user_id = ?", userID).First(&userPin).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &userPin, nil
}
func (r *userPinRepository) Create(ctx context.Context, userPin *model.UserPin) error {
err := r.db.WithContext(ctx).Create(userPin).Error
if err != nil {
return err
}
return nil
}
func (r *userPinRepository) Update(ctx context.Context, userPin *model.UserPin) error {
err := r.db.WithContext(ctx).Save(userPin).Error
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,23 @@
package userpin
import (
"rijig/config"
"rijig/internal/authentication"
"rijig/middleware"
"github.com/gofiber/fiber/v2"
)
func UsersPinRoute(api fiber.Router) {
userPinRepo := NewUserPinRepository(config.DB)
authRepo := authentication.NewAuthenticationRepository(config.DB)
userPinService := NewUserPinService(userPinRepo, authRepo)
userPinHandler := NewUserPinHandler(userPinService)
pin := api.Group("/pin", middleware.AuthMiddleware())
pin.Post("/create", userPinHandler.CreateUserPinHandler)
pin.Post("/verif", userPinHandler.VerifyPinHandler)
}

View File

@ -0,0 +1,97 @@
package userpin
import (
"context"
"fmt"
"rijig/internal/authentication"
"rijig/model"
"rijig/utils"
"strings"
)
type UserPinService interface {
CreateUserPin(ctx context.Context, userID string, dto *RequestPinDTO) error
VerifyUserPin(ctx context.Context, userID string, pin *RequestPinDTO) (*utils.TokenResponse, error)
}
type userPinService struct {
UserPinRepo UserPinRepository
authRepo authentication.AuthenticationRepository
}
func NewUserPinService(UserPinRepo UserPinRepository,
authRepo authentication.AuthenticationRepository) UserPinService {
return &userPinService{UserPinRepo, authRepo}
}
func (s *userPinService) CreateUserPin(ctx context.Context, userID string, dto *RequestPinDTO) error {
if errs, ok := dto.ValidateRequestPinDTO(); !ok {
return fmt.Errorf("validation error: %v", errs)
}
existingPin, err := s.UserPinRepo.FindByUserID(ctx, userID)
if err != nil {
return fmt.Errorf("failed to check existing PIN: %w", err)
}
if existingPin != nil {
return fmt.Errorf("PIN already created")
}
hashed, err := utils.HashingPlainText(dto.Pin)
if err != nil {
return fmt.Errorf("failed to hash PIN: %w", err)
}
userPin := &model.UserPin{
UserID: userID,
Pin: hashed,
}
if err := s.UserPinRepo.Create(ctx, userPin); err != nil {
return fmt.Errorf("failed to create PIN: %w", err)
}
user, err := s.authRepo.FindUserByID(ctx, userID)
if err != nil {
return fmt.Errorf("user not found")
}
roleName := strings.ToLower(user.Role.RoleName)
progress := authentication.IsRegistrationComplete(roleName, int(user.RegistrationProgress))
// progress := utils.GetNextRegistrationStep(roleName, int(user.RegistrationProgress))
// progress := utils.GetNextRegistrationStep(roleName, user.RegistrationProgress)
// progress := utils.GetNextRegistrationStep(roleName, user.RegistrationProgress)
if !progress {
err = s.authRepo.PatchUser(ctx, userID, map[string]interface{}{
"registration_progress": int(user.RegistrationProgress) + 1,
"registration_status": utils.RegStatusComplete,
})
if err != nil {
return fmt.Errorf("failed to update user progress: %w", err)
}
}
return nil
}
func (s *userPinService) VerifyUserPin(ctx context.Context, userID string, pin *RequestPinDTO) (*utils.TokenResponse, error) {
user, err := s.authRepo.FindUserByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("user not found")
}
userPin, err := s.UserPinRepo.FindByUserID(ctx, userID)
if err != nil || userPin == nil {
return nil, fmt.Errorf("PIN not found")
}
if !utils.CompareHashAndPlainText(userPin.Pin, pin.Pin) {
return nil, fmt.Errorf("PIN does not match, %s , %s", userPin.Pin, pin.Pin)
}
roleName := strings.ToLower(user.Role.RoleName)
return utils.GenerateTokenPair(user.ID, roleName, pin.DeviceId, user.RegistrationStatus, int(user.RegistrationProgress))
}

View File

@ -0,0 +1,67 @@
package userprofile
import (
"rijig/internal/role"
"rijig/utils"
"strings"
)
type UserProfileResponseDTO struct {
ID string `json:"id,omitempty"`
Avatar string `json:"avatar,omitempty"`
Name string `json:"name,omitempty"`
Gender string `json:"gender,omitempty"`
Dateofbirth string `json:"dateofbirth,omitempty"`
Placeofbirth string `json:"placeofbirth,omitempty"`
Phone string `json:"phone,omitempty"`
Email string `json:"email,omitempty"`
PhoneVerified bool `json:"phone_verified,omitempty"`
Password string `json:"password,omitempty"`
Role role.RoleResponseDTO `json:"role"`
CreatedAt string `json:"createdAt,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
}
type RequestUserProfileDTO struct {
Name string `json:"name"`
Gender string `json:"gender"`
Dateofbirth string `json:"dateofbirth"`
Placeofbirth string `json:"placeofbirth"`
Phone string `json:"phone"`
}
func (r *RequestUserProfileDTO) ValidateRequestUserProfileDTO() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Name) == "" {
errors["name"] = append(errors["name"], "Name is required")
}
if strings.TrimSpace(r.Gender) == "" {
errors["gender"] = append(errors["gender"], "jenis kelamin tidak boleh kosong")
} else if r.Gender != "perempuan" && r.Gender != "laki-laki" {
errors["gender"] = append(errors["gender"], "jenis kelamin harus 'perempuan' atau 'laki-laki'")
}
if strings.TrimSpace(r.Dateofbirth) == "" {
errors["dateofbirth"] = append(errors["dateofbirth"], "tanggal lahir dibutuhkan")
} else if !utils.IsValidDate(r.Dateofbirth) {
errors["dateofbirth"] = append(errors["dateofbirth"], "tanggal lahir harus berformat DD-MM-YYYY")
}
if strings.TrimSpace(r.Placeofbirth) == "" {
errors["placeofbirth"] = append(errors["placeofbirth"], "Name is required")
}
if strings.TrimSpace(r.Phone) == "" {
errors["phone"] = append(errors["phone"], "Phone number is required")
} else if !utils.IsValidPhoneNumber(r.Phone) {
errors["phone"] = append(errors["phone"], "Invalid phone number format. Use 62 followed by 9-13 digits")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1 @@
package userprofile

View File

@ -0,0 +1,35 @@
package userprofile
import (
"context"
"rijig/model"
"gorm.io/gorm"
)
type AuthenticationRepository interface {
UpdateUser(ctx context.Context, user *model.User) error
PatchUser(ctx context.Context, userID string, updates map[string]interface{}) error
}
type authenticationRepository struct {
db *gorm.DB
}
func NewAuthenticationRepository(db *gorm.DB) AuthenticationRepository {
return &authenticationRepository{db}
}
func (r *authenticationRepository) UpdateUser(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).
Model(&model.User{}).
Where("id = ?", user.ID).
Updates(user).Error
}
func (r *authenticationRepository) PatchUser(ctx context.Context, userID string, updates map[string]interface{}) error {
return r.db.WithContext(ctx).
Model(&model.User{}).
Where("id = ?", userID).
Updates(updates).Error
}

View File

@ -0,0 +1 @@
package userprofile

View File

@ -0,0 +1 @@
package userprofile

View File

@ -0,0 +1,24 @@
package whatsapp
import (
"log"
"rijig/config"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
func WhatsAppHandler(c *fiber.Ctx) error {
userID, ok := c.Locals("userID").(string)
if !ok || userID == "" {
return utils.Unauthorized(c, "User is not logged in or invalid session")
}
err := config.LogoutWhatsApp()
if err != nil {
log.Printf("Error during logout process for user %s: %v", userID, err)
return utils.InternalServerError(c, err.Error())
}
return utils.Success(c, "Logged out successfully")
}

View File

@ -0,0 +1,11 @@
package whatsapp
import (
"rijig/middleware"
"github.com/gofiber/fiber/v2"
)
func WhatsAppRouter(api fiber.Router) {
api.Post("/logout/whastapp", middleware.AuthMiddleware(), WhatsAppHandler)
}

View File

@ -0,0 +1,27 @@
package wilayahindo
type ProvinceResponseDTO struct {
ID string `json:"id"`
Name string `json:"name"`
Regencies []RegencyResponseDTO `json:"regencies,omitempty"`
}
type RegencyResponseDTO struct {
ID string `json:"id"`
ProvinceID string `json:"province_id"`
Name string `json:"name"`
Districts []DistrictResponseDTO `json:"districts,omitempty"`
}
type DistrictResponseDTO struct {
ID string `json:"id"`
RegencyID string `json:"regency_id"`
Name string `json:"name"`
Villages []VillageResponseDTO `json:"villages,omitempty"`
}
type VillageResponseDTO struct {
ID string `json:"id"`
DistrictID string `json:"district_id"`
Name string `json:"name"`
}

View File

@ -0,0 +1 @@
package wilayahindo

View File

@ -0,0 +1,310 @@
package wilayahindo
import (
"context"
"errors"
"fmt"
"rijig/model"
"gorm.io/gorm"
)
type WilayahIndonesiaRepository interface {
ImportProvinces(ctx context.Context, provinces []model.Province) error
ImportRegencies(ctx context.Context, regencies []model.Regency) error
ImportDistricts(ctx context.Context, districts []model.District) error
ImportVillages(ctx context.Context, villages []model.Village) error
FindAllProvinces(ctx context.Context, page, limit int) ([]model.Province, int, error)
FindProvinceByID(ctx context.Context, id string, page, limit int) (*model.Province, int, error)
FindAllRegencies(ctx context.Context, page, limit int) ([]model.Regency, int, error)
FindRegencyByID(ctx context.Context, id string, page, limit int) (*model.Regency, int, error)
FindAllDistricts(ctx context.Context, page, limit int) ([]model.District, int, error)
FindDistrictByID(ctx context.Context, id string, page, limit int) (*model.District, int, error)
FindAllVillages(ctx context.Context, page, limit int) ([]model.Village, int, error)
FindVillageByID(ctx context.Context, id string) (*model.Village, error)
}
type wilayahIndonesiaRepository struct {
DB *gorm.DB
}
func NewWilayahIndonesiaRepository(db *gorm.DB) WilayahIndonesiaRepository {
return &wilayahIndonesiaRepository{DB: db}
}
func (r *wilayahIndonesiaRepository) ImportProvinces(ctx context.Context, provinces []model.Province) error {
if len(provinces) == 0 {
return errors.New("no provinces to import")
}
if err := r.DB.WithContext(ctx).CreateInBatches(provinces, 100).Error; err != nil {
return fmt.Errorf("failed to import provinces: %w", err)
}
return nil
}
func (r *wilayahIndonesiaRepository) ImportRegencies(ctx context.Context, regencies []model.Regency) error {
if len(regencies) == 0 {
return errors.New("no regencies to import")
}
if err := r.DB.WithContext(ctx).CreateInBatches(regencies, 100).Error; err != nil {
return fmt.Errorf("failed to import regencies: %w", err)
}
return nil
}
func (r *wilayahIndonesiaRepository) ImportDistricts(ctx context.Context, districts []model.District) error {
if len(districts) == 0 {
return errors.New("no districts to import")
}
if err := r.DB.WithContext(ctx).CreateInBatches(districts, 100).Error; err != nil {
return fmt.Errorf("failed to import districts: %w", err)
}
return nil
}
func (r *wilayahIndonesiaRepository) ImportVillages(ctx context.Context, villages []model.Village) error {
if len(villages) == 0 {
return errors.New("no villages to import")
}
if err := r.DB.WithContext(ctx).CreateInBatches(villages, 100).Error; err != nil {
return fmt.Errorf("failed to import villages: %w", err)
}
return nil
}
func (r *wilayahIndonesiaRepository) FindAllProvinces(ctx context.Context, page, limit int) ([]model.Province, int, error) {
var provinces []model.Province
var total int64
if err := r.DB.WithContext(ctx).Model(&model.Province{}).Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count provinces: %w", err)
}
query := r.DB.WithContext(ctx)
if page > 0 && limit > 0 {
if page < 1 {
return nil, 0, errors.New("page must be greater than 0")
}
if limit < 1 || limit > 1000 {
return nil, 0, errors.New("limit must be between 1 and 1000")
}
query = query.Offset((page - 1) * limit).Limit(limit)
}
if err := query.Find(&provinces).Error; err != nil {
return nil, 0, fmt.Errorf("failed to find provinces: %w", err)
}
return provinces, int(total), nil
}
func (r *wilayahIndonesiaRepository) FindProvinceByID(ctx context.Context, id string, page, limit int) (*model.Province, int, error) {
if id == "" {
return nil, 0, errors.New("province ID cannot be empty")
}
var province model.Province
preloadQuery := func(db *gorm.DB) *gorm.DB {
if page > 0 && limit > 0 {
if page < 1 {
return db
}
if limit < 1 || limit > 1000 {
return db
}
return db.Offset((page - 1) * limit).Limit(limit)
}
return db
}
if err := r.DB.WithContext(ctx).Preload("Regencies", preloadQuery).Where("id = ?", id).First(&province).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, fmt.Errorf("province with ID %s not found", id)
}
return nil, 0, fmt.Errorf("failed to find province: %w", err)
}
var totalRegencies int64
if err := r.DB.WithContext(ctx).Model(&model.Regency{}).Where("province_id = ?", id).Count(&totalRegencies).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count regencies: %w", err)
}
return &province, int(totalRegencies), nil
}
func (r *wilayahIndonesiaRepository) FindAllRegencies(ctx context.Context, page, limit int) ([]model.Regency, int, error) {
var regencies []model.Regency
var total int64
if err := r.DB.WithContext(ctx).Model(&model.Regency{}).Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count regencies: %w", err)
}
query := r.DB.WithContext(ctx)
if page > 0 && limit > 0 {
if page < 1 {
return nil, 0, errors.New("page must be greater than 0")
}
if limit < 1 || limit > 1000 {
return nil, 0, errors.New("limit must be between 1 and 1000")
}
query = query.Offset((page - 1) * limit).Limit(limit)
}
if err := query.Find(&regencies).Error; err != nil {
return nil, 0, fmt.Errorf("failed to find regencies: %w", err)
}
return regencies, int(total), nil
}
func (r *wilayahIndonesiaRepository) FindRegencyByID(ctx context.Context, id string, page, limit int) (*model.Regency, int, error) {
if id == "" {
return nil, 0, errors.New("regency ID cannot be empty")
}
var regency model.Regency
preloadQuery := func(db *gorm.DB) *gorm.DB {
if page > 0 && limit > 0 {
if page < 1 {
return db
}
if limit < 1 || limit > 1000 {
return db
}
return db.Offset((page - 1) * limit).Limit(limit)
}
return db
}
if err := r.DB.WithContext(ctx).Preload("Districts", preloadQuery).Where("id = ?", id).First(&regency).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, fmt.Errorf("regency with ID %s not found", id)
}
return nil, 0, fmt.Errorf("failed to find regency: %w", err)
}
var totalDistricts int64
if err := r.DB.WithContext(ctx).Model(&model.District{}).Where("regency_id = ?", id).Count(&totalDistricts).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count districts: %w", err)
}
return &regency, int(totalDistricts), nil
}
func (r *wilayahIndonesiaRepository) FindAllDistricts(ctx context.Context, page, limit int) ([]model.District, int, error) {
var districts []model.District
var total int64
if err := r.DB.WithContext(ctx).Model(&model.District{}).Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count districts: %w", err)
}
query := r.DB.WithContext(ctx)
if page > 0 && limit > 0 {
if page < 1 {
return nil, 0, errors.New("page must be greater than 0")
}
if limit < 1 || limit > 1000 {
return nil, 0, errors.New("limit must be between 1 and 1000")
}
query = query.Offset((page - 1) * limit).Limit(limit)
}
if err := query.Find(&districts).Error; err != nil {
return nil, 0, fmt.Errorf("failed to find districts: %w", err)
}
return districts, int(total), nil
}
func (r *wilayahIndonesiaRepository) FindDistrictByID(ctx context.Context, id string, page, limit int) (*model.District, int, error) {
if id == "" {
return nil, 0, errors.New("district ID cannot be empty")
}
var district model.District
preloadQuery := func(db *gorm.DB) *gorm.DB {
if page > 0 && limit > 0 {
if page < 1 {
return db
}
if limit < 1 || limit > 1000 {
return db
}
return db.Offset((page - 1) * limit).Limit(limit)
}
return db
}
if err := r.DB.WithContext(ctx).Preload("Villages", preloadQuery).Where("id = ?", id).First(&district).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, fmt.Errorf("district with ID %s not found", id)
}
return nil, 0, fmt.Errorf("failed to find district: %w", err)
}
var totalVillages int64
if err := r.DB.WithContext(ctx).Model(&model.Village{}).Where("district_id = ?", id).Count(&totalVillages).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count villages: %w", err)
}
return &district, int(totalVillages), nil
}
func (r *wilayahIndonesiaRepository) FindAllVillages(ctx context.Context, page, limit int) ([]model.Village, int, error) {
var villages []model.Village
var total int64
if err := r.DB.WithContext(ctx).Model(&model.Village{}).Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count villages: %w", err)
}
query := r.DB.WithContext(ctx)
if page > 0 && limit > 0 {
if page < 1 {
return nil, 0, errors.New("page must be greater than 0")
}
if limit < 1 || limit > 1000 {
return nil, 0, errors.New("limit must be between 1 and 1000")
}
query = query.Offset((page - 1) * limit).Limit(limit)
}
if err := query.Find(&villages).Error; err != nil {
return nil, 0, fmt.Errorf("failed to find villages: %w", err)
}
return villages, int(total), nil
}
func (r *wilayahIndonesiaRepository) FindVillageByID(ctx context.Context, id string) (*model.Village, error) {
if id == "" {
return nil, errors.New("village ID cannot be empty")
}
var village model.Village
if err := r.DB.WithContext(ctx).Where("id = ?", id).First(&village).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("village with ID %s not found", id)
}
return nil, fmt.Errorf("failed to find village: %w", err)
}
return &village, nil
}

View File

@ -0,0 +1 @@
package wilayahindo

View File

@ -0,0 +1,455 @@
package wilayahindo
import (
"context"
"fmt"
"time"
"rijig/dto"
"rijig/model"
"rijig/utils"
)
type WilayahIndonesiaService interface {
ImportDataFromCSV(ctx context.Context) error
GetAllProvinces(ctx context.Context, page, limit int) ([]dto.ProvinceResponseDTO, int, error)
GetProvinceByID(ctx context.Context, id string, page, limit int) (*dto.ProvinceResponseDTO, int, error)
GetAllRegencies(ctx context.Context, page, limit int) ([]dto.RegencyResponseDTO, int, error)
GetRegencyByID(ctx context.Context, id string, page, limit int) (*dto.RegencyResponseDTO, int, error)
GetAllDistricts(ctx context.Context, page, limit int) ([]dto.DistrictResponseDTO, int, error)
GetDistrictByID(ctx context.Context, id string, page, limit int) (*dto.DistrictResponseDTO, int, error)
GetAllVillages(ctx context.Context, page, limit int) ([]dto.VillageResponseDTO, int, error)
GetVillageByID(ctx context.Context, id string) (*dto.VillageResponseDTO, error)
}
type wilayahIndonesiaService struct {
WilayahRepo WilayahIndonesiaRepository
}
func NewWilayahIndonesiaService(wilayahRepo WilayahIndonesiaRepository) WilayahIndonesiaService {
return &wilayahIndonesiaService{WilayahRepo: wilayahRepo}
}
func (s *wilayahIndonesiaService) ImportDataFromCSV(ctx context.Context) error {
provinces, err := utils.ReadCSV("public/document/provinces.csv")
if err != nil {
return fmt.Errorf("failed to read provinces CSV: %w", err)
}
var provinceList []model.Province
for _, record := range provinces[1:] {
if len(record) >= 2 {
province := model.Province{
ID: record[0],
Name: record[1],
}
provinceList = append(provinceList, province)
}
}
if err := s.WilayahRepo.ImportProvinces(ctx, provinceList); err != nil {
return fmt.Errorf("failed to import provinces: %w", err)
}
regencies, err := utils.ReadCSV("public/document/regencies.csv")
if err != nil {
return fmt.Errorf("failed to read regencies CSV: %w", err)
}
var regencyList []model.Regency
for _, record := range regencies[1:] {
if len(record) >= 3 {
regency := model.Regency{
ID: record[0],
ProvinceID: record[1],
Name: record[2],
}
regencyList = append(regencyList, regency)
}
}
if err := s.WilayahRepo.ImportRegencies(ctx, regencyList); err != nil {
return fmt.Errorf("failed to import regencies: %w", err)
}
districts, err := utils.ReadCSV("public/document/districts.csv")
if err != nil {
return fmt.Errorf("failed to read districts CSV: %w", err)
}
var districtList []model.District
for _, record := range districts[1:] {
if len(record) >= 3 {
district := model.District{
ID: record[0],
RegencyID: record[1],
Name: record[2],
}
districtList = append(districtList, district)
}
}
if err := s.WilayahRepo.ImportDistricts(ctx, districtList); err != nil {
return fmt.Errorf("failed to import districts: %w", err)
}
villages, err := utils.ReadCSV("public/document/villages.csv")
if err != nil {
return fmt.Errorf("failed to read villages CSV: %w", err)
}
var villageList []model.Village
for _, record := range villages[1:] {
if len(record) >= 3 {
village := model.Village{
ID: record[0],
DistrictID: record[1],
Name: record[2],
}
villageList = append(villageList, village)
}
}
if err := s.WilayahRepo.ImportVillages(ctx, villageList); err != nil {
return fmt.Errorf("failed to import villages: %w", err)
}
return nil
}
func (s *wilayahIndonesiaService) GetAllProvinces(ctx context.Context, page, limit int) ([]dto.ProvinceResponseDTO, int, error) {
cacheKey := fmt.Sprintf("provinces_page:%d_limit:%d", page, limit)
var cachedResponse struct {
Data []dto.ProvinceResponseDTO `json:"data"`
Total int `json:"total"`
}
if err := utils.GetCache(cacheKey, &cachedResponse); err == nil {
return cachedResponse.Data, cachedResponse.Total, nil
}
provinces, total, err := s.WilayahRepo.FindAllProvinces(ctx, page, limit)
if err != nil {
return nil, 0, fmt.Errorf("failed to fetch provinces: %w", err)
}
provinceDTOs := make([]dto.ProvinceResponseDTO, len(provinces))
for i, province := range provinces {
provinceDTOs[i] = dto.ProvinceResponseDTO{
ID: province.ID,
Name: province.Name,
}
}
cacheData := struct {
Data []dto.ProvinceResponseDTO `json:"data"`
Total int `json:"total"`
}{
Data: provinceDTOs,
Total: total,
}
if err := utils.SetCache(cacheKey, cacheData, 24*time.Hour); err != nil {
fmt.Printf("Error caching provinces data: %v\n", err)
}
return provinceDTOs, total, nil
}
func (s *wilayahIndonesiaService) GetProvinceByID(ctx context.Context, id string, page, limit int) (*dto.ProvinceResponseDTO, int, error) {
cacheKey := fmt.Sprintf("province:%s_page:%d_limit:%d", id, page, limit)
var cachedResponse struct {
Data dto.ProvinceResponseDTO `json:"data"`
TotalRegencies int `json:"total_regencies"`
}
if err := utils.GetCache(cacheKey, &cachedResponse); err == nil {
return &cachedResponse.Data, cachedResponse.TotalRegencies, nil
}
province, totalRegencies, err := s.WilayahRepo.FindProvinceByID(ctx, id, page, limit)
if err != nil {
return nil, 0, err
}
provinceDTO := dto.ProvinceResponseDTO{
ID: province.ID,
Name: province.Name,
}
regencyDTOs := make([]dto.RegencyResponseDTO, len(province.Regencies))
for i, regency := range province.Regencies {
regencyDTOs[i] = dto.RegencyResponseDTO{
ID: regency.ID,
ProvinceID: regency.ProvinceID,
Name: regency.Name,
}
}
provinceDTO.Regencies = regencyDTOs
cacheData := struct {
Data dto.ProvinceResponseDTO `json:"data"`
TotalRegencies int `json:"total_regencies"`
}{
Data: provinceDTO,
TotalRegencies: totalRegencies,
}
if err := utils.SetCache(cacheKey, cacheData, 24*time.Hour); err != nil {
fmt.Printf("Error caching province data: %v\n", err)
}
return &provinceDTO, totalRegencies, nil
}
func (s *wilayahIndonesiaService) GetAllRegencies(ctx context.Context, page, limit int) ([]dto.RegencyResponseDTO, int, error) {
cacheKey := fmt.Sprintf("regencies_page:%d_limit:%d", page, limit)
var cachedResponse struct {
Data []dto.RegencyResponseDTO `json:"data"`
Total int `json:"total"`
}
if err := utils.GetCache(cacheKey, &cachedResponse); err == nil {
return cachedResponse.Data, cachedResponse.Total, nil
}
regencies, total, err := s.WilayahRepo.FindAllRegencies(ctx, page, limit)
if err != nil {
return nil, 0, fmt.Errorf("failed to fetch regencies: %w", err)
}
regencyDTOs := make([]dto.RegencyResponseDTO, len(regencies))
for i, regency := range regencies {
regencyDTOs[i] = dto.RegencyResponseDTO{
ID: regency.ID,
ProvinceID: regency.ProvinceID,
Name: regency.Name,
}
}
cacheData := struct {
Data []dto.RegencyResponseDTO `json:"data"`
Total int `json:"total"`
}{
Data: regencyDTOs,
Total: total,
}
if err := utils.SetCache(cacheKey, cacheData, 24*time.Hour); err != nil {
fmt.Printf("Error caching regencies data: %v\n", err)
}
return regencyDTOs, total, nil
}
func (s *wilayahIndonesiaService) GetRegencyByID(ctx context.Context, id string, page, limit int) (*dto.RegencyResponseDTO, int, error) {
cacheKey := fmt.Sprintf("regency:%s_page:%d_limit:%d", id, page, limit)
var cachedResponse struct {
Data dto.RegencyResponseDTO `json:"data"`
TotalDistricts int `json:"total_districts"`
}
if err := utils.GetCache(cacheKey, &cachedResponse); err == nil {
return &cachedResponse.Data, cachedResponse.TotalDistricts, nil
}
regency, totalDistricts, err := s.WilayahRepo.FindRegencyByID(ctx, id, page, limit)
if err != nil {
return nil, 0, err
}
regencyDTO := dto.RegencyResponseDTO{
ID: regency.ID,
ProvinceID: regency.ProvinceID,
Name: regency.Name,
}
districtDTOs := make([]dto.DistrictResponseDTO, len(regency.Districts))
for i, district := range regency.Districts {
districtDTOs[i] = dto.DistrictResponseDTO{
ID: district.ID,
RegencyID: district.RegencyID,
Name: district.Name,
}
}
regencyDTO.Districts = districtDTOs
cacheData := struct {
Data dto.RegencyResponseDTO `json:"data"`
TotalDistricts int `json:"total_districts"`
}{
Data: regencyDTO,
TotalDistricts: totalDistricts,
}
if err := utils.SetCache(cacheKey, cacheData, 24*time.Hour); err != nil {
fmt.Printf("Error caching regency data: %v\n", err)
}
return &regencyDTO, totalDistricts, nil
}
func (s *wilayahIndonesiaService) GetAllDistricts(ctx context.Context, page, limit int) ([]dto.DistrictResponseDTO, int, error) {
cacheKey := fmt.Sprintf("districts_page:%d_limit:%d", page, limit)
var cachedResponse struct {
Data []dto.DistrictResponseDTO `json:"data"`
Total int `json:"total"`
}
if err := utils.GetCache(cacheKey, &cachedResponse); err == nil {
return cachedResponse.Data, cachedResponse.Total, nil
}
districts, total, err := s.WilayahRepo.FindAllDistricts(ctx, page, limit)
if err != nil {
return nil, 0, fmt.Errorf("failed to fetch districts: %w", err)
}
districtDTOs := make([]dto.DistrictResponseDTO, len(districts))
for i, district := range districts {
districtDTOs[i] = dto.DistrictResponseDTO{
ID: district.ID,
RegencyID: district.RegencyID,
Name: district.Name,
}
}
cacheData := struct {
Data []dto.DistrictResponseDTO `json:"data"`
Total int `json:"total"`
}{
Data: districtDTOs,
Total: total,
}
if err := utils.SetCache(cacheKey, cacheData, 24*time.Hour); err != nil {
fmt.Printf("Error caching districts data: %v\n", err)
}
return districtDTOs, total, nil
}
func (s *wilayahIndonesiaService) GetDistrictByID(ctx context.Context, id string, page, limit int) (*dto.DistrictResponseDTO, int, error) {
cacheKey := fmt.Sprintf("district:%s_page:%d_limit:%d", id, page, limit)
var cachedResponse struct {
Data dto.DistrictResponseDTO `json:"data"`
TotalVillages int `json:"total_villages"`
}
if err := utils.GetCache(cacheKey, &cachedResponse); err == nil {
return &cachedResponse.Data, cachedResponse.TotalVillages, nil
}
district, totalVillages, err := s.WilayahRepo.FindDistrictByID(ctx, id, page, limit)
if err != nil {
return nil, 0, err
}
districtDTO := dto.DistrictResponseDTO{
ID: district.ID,
RegencyID: district.RegencyID,
Name: district.Name,
}
villageDTOs := make([]dto.VillageResponseDTO, len(district.Villages))
for i, village := range district.Villages {
villageDTOs[i] = dto.VillageResponseDTO{
ID: village.ID,
DistrictID: village.DistrictID,
Name: village.Name,
}
}
districtDTO.Villages = villageDTOs
cacheData := struct {
Data dto.DistrictResponseDTO `json:"data"`
TotalVillages int `json:"total_villages"`
}{
Data: districtDTO,
TotalVillages: totalVillages,
}
if err := utils.SetCache(cacheKey, cacheData, 24*time.Hour); err != nil {
fmt.Printf("Error caching district data: %v\n", err)
}
return &districtDTO, totalVillages, nil
}
func (s *wilayahIndonesiaService) GetAllVillages(ctx context.Context, page, limit int) ([]dto.VillageResponseDTO, int, error) {
cacheKey := fmt.Sprintf("villages_page:%d_limit:%d", page, limit)
var cachedResponse struct {
Data []dto.VillageResponseDTO `json:"data"`
Total int `json:"total"`
}
if err := utils.GetCache(cacheKey, &cachedResponse); err == nil {
return cachedResponse.Data, cachedResponse.Total, nil
}
villages, total, err := s.WilayahRepo.FindAllVillages(ctx, page, limit)
if err != nil {
return nil, 0, fmt.Errorf("failed to fetch villages: %w", err)
}
villageDTOs := make([]dto.VillageResponseDTO, len(villages))
for i, village := range villages {
villageDTOs[i] = dto.VillageResponseDTO{
ID: village.ID,
DistrictID: village.DistrictID,
Name: village.Name,
}
}
cacheData := struct {
Data []dto.VillageResponseDTO `json:"data"`
Total int `json:"total"`
}{
Data: villageDTOs,
Total: total,
}
if err := utils.SetCache(cacheKey, cacheData, 24*time.Hour); err != nil {
fmt.Printf("Error caching villages data: %v\n", err)
}
return villageDTOs, total, nil
}
func (s *wilayahIndonesiaService) GetVillageByID(ctx context.Context, id string) (*dto.VillageResponseDTO, error) {
cacheKey := fmt.Sprintf("village:%s", id)
var cachedResponse dto.VillageResponseDTO
if err := utils.GetCache(cacheKey, &cachedResponse); err == nil {
return &cachedResponse, nil
}
village, err := s.WilayahRepo.FindVillageByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("village not found: %w", err)
}
villageResponse := &dto.VillageResponseDTO{
ID: village.ID,
DistrictID: village.DistrictID,
Name: village.Name,
}
if err := utils.SetCache(cacheKey, villageResponse, 24*time.Hour); err != nil {
fmt.Printf("Error caching village data: %v\n", err)
}
return villageResponse, nil
}

View File

@ -0,0 +1,199 @@
package middleware
/*
import (
"fmt"
"time"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
func RateLimitByUser(maxRequests int, duration time.Duration) fiber.Handler {
return func(c *fiber.Ctx) error {
claims, err := GetUserFromContext(c)
if err != nil {
return err
}
key := fmt.Sprintf("rate_limit:%s:%s", claims.UserID, c.Route().Path)
count, err := utils.IncrementCounter(key, duration)
if err != nil {
return c.Next()
}
if count > int64(maxRequests) {
ttl, _ := utils.GetTTL(key)
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
"error": "Rate limit exceeded",
"message": "Terlalu banyak permintaan, silakan coba lagi nanti",
"retry_after": int64(ttl.Seconds()),
"limit": maxRequests,
"remaining": 0,
})
}
c.Set("X-RateLimit-Limit", fmt.Sprintf("%d", maxRequests))
c.Set("X-RateLimit-Remaining", fmt.Sprintf("%d", maxRequests-int(count)))
c.Set("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(duration).Unix()))
return c.Next()
}
}
func SessionValidation() fiber.Handler {
return func(c *fiber.Ctx) error {
claims, err := GetUserFromContext(c)
if err != nil {
return err
}
if claims.SessionID == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Invalid session",
"message": "Session tidak valid",
})
}
sessionKey := fmt.Sprintf("session:%s", claims.SessionID)
var sessionData map[string]interface{}
err = utils.GetCache(sessionKey, &sessionData)
if err != nil {
if err.Error() == "ErrCacheMiss" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Session not found",
"message": "Session tidak ditemukan, silakan login kembali",
})
}
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Session error",
"message": "Terjadi kesalahan saat validasi session",
})
}
if sessionData["user_id"] != claims.UserID {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Session mismatch",
"message": "Session tidak sesuai dengan user",
})
}
if expiryInterface, exists := sessionData["expires_at"]; exists {
if expiry, ok := expiryInterface.(float64); ok {
if time.Now().Unix() > int64(expiry) {
utils.DeleteCache(sessionKey)
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Session expired",
"message": "Session telah berakhir, silakan login kembali",
})
}
}
}
return c.Next()
}
}
func RequireApprovedRegistration() fiber.Handler {
return func(c *fiber.Ctx) error {
claims, err := GetUserFromContext(c)
if err != nil {
return err
}
if claims.RegistrationStatus == utils.RegStatusRejected {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "Registration rejected",
"message": "Registrasi Anda ditolak, silakan hubungi admin",
})
}
if claims.RegistrationStatus == utils.RegStatusPending {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "Registration pending",
"message": "Registrasi Anda masih menunggu persetujuan admin",
})
}
if claims.RegistrationStatus != utils.RegStatusComplete {
progress := utils.GetUserRegistrationProgress(claims.UserID)
nextStep := utils.GetNextRegistrationStep(claims.Role, progress)
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "Registration incomplete",
"message": "Silakan lengkapi registrasi terlebih dahulu",
"registration_status": claims.RegistrationStatus,
"next_step": nextStep,
})
}
return c.Next()
}
}
func ConditionalAuth(condition func(*utils.JWTClaims) bool, errorMessage string) fiber.Handler {
return func(c *fiber.Ctx) error {
claims, err := GetUserFromContext(c)
if err != nil {
return err
}
if !condition(claims) {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "Condition not met",
"message": errorMessage,
})
}
return c.Next()
}
}
func RequireSpecificRole(role string) fiber.Handler {
return ConditionalAuth(
func(claims *utils.JWTClaims) bool {
return claims.Role == role
},
fmt.Sprintf("Akses ini hanya untuk role %s", role),
)
}
func RequireCompleteRegistrationAndSpecificRole(role string) fiber.Handler {
return ConditionalAuth(
func(claims *utils.JWTClaims) bool {
return claims.Role == role && utils.IsRegistrationComplete(claims.RegistrationStatus)
},
fmt.Sprintf("Akses ini hanya untuk role %s dengan registrasi lengkap", role),
)
}
func DeviceValidation() fiber.Handler {
return func(c *fiber.Ctx) error {
claims, err := GetUserFromContext(c)
if err != nil {
return err
}
deviceID := c.Get("X-Device-ID")
if deviceID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Device ID required",
"message": "Device ID diperlukan",
})
}
if claims.DeviceID != deviceID {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Device mismatch",
"message": "Token tidak valid untuk device ini",
})
}
return c.Next()
}
}
*/

View File

@ -11,12 +11,12 @@ import (
func APIKeyMiddleware(c *fiber.Ctx) error { func APIKeyMiddleware(c *fiber.Ctx) error {
apiKey := c.Get("x-api-key") apiKey := c.Get("x-api-key")
if apiKey == "" { if apiKey == "" {
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: API key is required") return utils.Unauthorized(c, "Unauthorized: API key is required")
} }
validAPIKey := os.Getenv("API_KEY") validAPIKey := os.Getenv("API_KEY")
if apiKey != validAPIKey { if apiKey != validAPIKey {
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid API key") return utils.Unauthorized(c, "Unauthorized: Invalid API key")
} }
return c.Next() return c.Next()

View File

@ -1,65 +0,0 @@
package middleware
import (
"fmt"
"log"
"os"
"rijig/utils"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
func AuthMiddleware(c *fiber.Ctx) error {
tokenString := c.Get("Authorization")
if tokenString == "" {
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: No token provided")
}
if len(tokenString) > 7 && tokenString[:7] == "Bearer " {
tokenString = tokenString[7:]
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("SECRET_KEY")), nil
})
if err != nil || !token.Valid {
log.Printf("Error parsing token: %v", err)
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid token")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || claims["sub"] == nil || claims["device_id"] == nil {
log.Println("Invalid token claims")
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid token claims")
}
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Unexpected signing method")
}
userID := claims["sub"].(string)
deviceID := claims["device_id"].(string)
sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID)
sessionData, err := utils.GetJSONData(sessionKey)
if err != nil || sessionData == nil {
log.Printf("Session expired or invalid for userID: %s, deviceID: %s", userID, deviceID)
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Session expired or invalid")
}
roleID, roleOK := sessionData["roleID"].(string)
roleName, roleNameOK := sessionData["roleName"].(string)
if !roleOK || !roleNameOK {
log.Println("Invalid session data for userID:", userID)
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid session data")
}
c.Locals("userID", userID)
c.Locals("roleID", roleID)
c.Locals("roleName", roleName)
c.Locals("device_id", deviceID)
return c.Next()
}

564
middleware/middleware.go Normal file
View File

@ -0,0 +1,564 @@
package middleware
import (
"crypto/subtle"
"fmt"
"rijig/utils"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
)
type AuthConfig struct {
RequiredTokenType utils.TokenType
RequiredRoles []string
RequiredStatuses []string
RequiredStep int
RequireComplete bool
SkipAuth bool
AllowPartialToken bool
CustomErrorHandler ErrorHandler
}
type ErrorHandler func(c *fiber.Ctx, err error) error
type AuthContext struct {
Claims *utils.JWTClaims
StepInfo *utils.RegistrationStepInfo
IsAdmin bool
CanAccess bool
}
type AuthError struct {
Code string `json:"error"`
Message string `json:"message"`
Details interface{} `json:"details,omitempty"`
}
func (e *AuthError) Error() string {
return e.Message
}
var (
ErrMissingToken = &AuthError{
Code: "MISSING_TOKEN",
Message: "Token akses diperlukan",
}
ErrInvalidTokenFormat = &AuthError{
Code: "INVALID_TOKEN_FORMAT",
Message: "Format token tidak valid",
}
ErrInvalidToken = &AuthError{
Code: "INVALID_TOKEN",
Message: "Token tidak valid atau telah kadaluarsa",
}
ErrUserContextNotFound = &AuthError{
Code: "USER_CONTEXT_NOT_FOUND",
Message: "Silakan login terlebih dahulu",
}
ErrInsufficientPermissions = &AuthError{
Code: "INSUFFICIENT_PERMISSIONS",
Message: "Akses ditolak untuk role ini",
}
ErrRegistrationIncomplete = &AuthError{
Code: "REGISTRATION_INCOMPLETE",
Message: "Registrasi belum lengkap",
}
ErrRegistrationNotApproved = &AuthError{
Code: "REGISTRATION_NOT_APPROVED",
Message: "Registrasi belum disetujui",
}
ErrInvalidTokenType = &AuthError{
Code: "INVALID_TOKEN_TYPE",
Message: "Tipe token tidak sesuai",
}
ErrStepNotAccessible = &AuthError{
Code: "STEP_NOT_ACCESSIBLE",
Message: "Step registrasi belum dapat diakses",
}
ErrAwaitingApproval = &AuthError{
Code: "AWAITING_ADMIN_APPROVAL",
Message: "Menunggu persetujuan admin",
}
ErrInvalidRegistrationStatus = &AuthError{
Code: "INVALID_REGISTRATION_STATUS",
Message: "Status registrasi tidak sesuai",
}
)
func defaultErrorHandler(c *fiber.Ctx, err error) error {
if authErr, ok := err.(*AuthError); ok {
statusCode := getStatusCodeForError(authErr.Code)
return c.Status(statusCode).JSON(authErr)
}
return c.Status(fiber.StatusInternalServerError).JSON(&AuthError{
Code: "INTERNAL_ERROR",
Message: "Terjadi kesalahan internal",
})
}
func getStatusCodeForError(errorCode string) int {
switch errorCode {
case "MISSING_TOKEN", "INVALID_TOKEN_FORMAT", "INVALID_TOKEN", "USER_CONTEXT_NOT_FOUND":
return fiber.StatusUnauthorized
case "INSUFFICIENT_PERMISSIONS", "REGISTRATION_INCOMPLETE", "REGISTRATION_NOT_APPROVED",
"INVALID_TOKEN_TYPE", "STEP_NOT_ACCESSIBLE", "AWAITING_ADMIN_APPROVAL",
"INVALID_REGISTRATION_STATUS":
return fiber.StatusForbidden
default:
return fiber.StatusInternalServerError
}
}
func AuthMiddleware(config ...AuthConfig) fiber.Handler {
cfg := AuthConfig{}
if len(config) > 0 {
cfg = config[0]
}
if cfg.CustomErrorHandler == nil {
cfg.CustomErrorHandler = defaultErrorHandler
}
return func(c *fiber.Ctx) error {
if cfg.SkipAuth {
return c.Next()
}
claims, err := extractAndValidateToken(c)
if err != nil {
return cfg.CustomErrorHandler(c, err)
}
authCtx := createAuthContext(claims)
if err := validateAuthConfig(authCtx, cfg); err != nil {
return cfg.CustomErrorHandler(c, err)
}
c.Locals("user", claims)
c.Locals("auth_context", authCtx)
return c.Next()
}
}
func extractAndValidateToken(c *fiber.Ctx) (*utils.JWTClaims, error) {
authHeader := c.Get("Authorization")
if authHeader == "" {
return nil, ErrMissingToken
}
token, err := utils.ExtractTokenFromHeader(authHeader)
if err != nil {
return nil, ErrInvalidTokenFormat
}
claims, err := utils.ValidateAccessToken(token)
if err != nil {
return nil, ErrInvalidToken
}
return claims, nil
}
func createAuthContext(claims *utils.JWTClaims) *AuthContext {
stepInfo := utils.GetRegistrationStepInfo(
claims.Role,
claims.RegistrationProgress,
claims.RegistrationStatus,
)
return &AuthContext{
Claims: claims,
StepInfo: stepInfo,
IsAdmin: claims.Role == utils.RoleAdministrator,
CanAccess: stepInfo.IsAccessible,
}
}
func validateAuthConfig(authCtx *AuthContext, cfg AuthConfig) error {
claims := authCtx.Claims
if cfg.RequiredTokenType != "" {
if claims.TokenType != cfg.RequiredTokenType {
return &AuthError{
Code: "INVALID_TOKEN_TYPE",
Message: fmt.Sprintf("Endpoint memerlukan token type: %s", cfg.RequiredTokenType),
Details: fiber.Map{
"current_token_type": claims.TokenType,
"required_token_type": cfg.RequiredTokenType,
},
}
}
}
if len(cfg.RequiredRoles) > 0 {
if !contains(cfg.RequiredRoles, claims.Role) {
return &AuthError{
Code: "INSUFFICIENT_PERMISSIONS",
Message: "Akses ditolak untuk role ini",
Details: fiber.Map{
"user_role": claims.Role,
"allowed_roles": cfg.RequiredRoles,
},
}
}
}
if len(cfg.RequiredStatuses) > 0 {
if !contains(cfg.RequiredStatuses, claims.RegistrationStatus) {
return &AuthError{
Code: "INVALID_REGISTRATION_STATUS",
Message: "Status registrasi tidak sesuai",
Details: fiber.Map{
"current_status": claims.RegistrationStatus,
"allowed_statuses": cfg.RequiredStatuses,
"next_step": authCtx.StepInfo.Description,
},
}
}
}
if cfg.RequiredStep > 0 {
if claims.RegistrationProgress < cfg.RequiredStep {
return &AuthError{
Code: "STEP_NOT_ACCESSIBLE",
Message: "Step registrasi belum dapat diakses",
Details: fiber.Map{
"current_step": claims.RegistrationProgress,
"required_step": cfg.RequiredStep,
"current_step_info": authCtx.StepInfo.Description,
},
}
}
if authCtx.StepInfo.RequiresAdminApproval && !authCtx.CanAccess {
return &AuthError{
Code: "AWAITING_ADMIN_APPROVAL",
Message: "Menunggu persetujuan admin",
Details: fiber.Map{
"status": claims.RegistrationStatus,
},
}
}
}
if cfg.RequireComplete {
if claims.TokenType != utils.TokenTypeFull {
return &AuthError{
Code: "REGISTRATION_INCOMPLETE",
Message: "Registrasi belum lengkap",
Details: fiber.Map{
"registration_status": claims.RegistrationStatus,
"registration_progress": claims.RegistrationProgress,
"next_step": authCtx.StepInfo.Description,
"requires_admin_approval": authCtx.StepInfo.RequiresAdminApproval,
"can_proceed": authCtx.CanAccess,
},
}
}
if !utils.IsRegistrationComplete(claims.RegistrationStatus) {
return ErrRegistrationNotApproved
}
}
return nil
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
func RequireAuth() fiber.Handler {
return AuthMiddleware()
}
func RequireFullToken() fiber.Handler {
return AuthMiddleware(AuthConfig{
RequiredTokenType: utils.TokenTypeFull,
RequireComplete: true,
})
}
func RequirePartialToken() fiber.Handler {
return AuthMiddleware(AuthConfig{
RequiredTokenType: utils.TokenTypePartial,
})
}
func RequireRoles(roles ...string) fiber.Handler {
return AuthMiddleware(AuthConfig{
RequiredRoles: roles,
})
}
func RequireAdminRole() fiber.Handler {
return RequireRoles(utils.RoleAdministrator)
}
func RequireRegistrationStep(step int) fiber.Handler {
return AuthMiddleware(AuthConfig{
RequiredStep: step,
})
}
func RequireRegistrationStatus(statuses ...string) fiber.Handler {
return AuthMiddleware(AuthConfig{
RequiredStatuses: statuses,
})
}
func RequireTokenType(tokenType utils.TokenType) fiber.Handler {
return AuthMiddleware(AuthConfig{
RequiredTokenType: tokenType,
})
}
func RequireCompleteRegistrationForRole(roles ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
claims, err := GetUserFromContext(c)
if err != nil {
return err
}
if contains(roles, claims.Role) {
return RequireFullToken()(c)
}
return c.Next()
}
}
func RequireRoleAndComplete(roles ...string) fiber.Handler {
return AuthMiddleware(AuthConfig{
RequiredRoles: roles,
RequireComplete: true,
})
}
func RequireRoleAndStep(step int, roles ...string) fiber.Handler {
return AuthMiddleware(AuthConfig{
RequiredRoles: roles,
RequiredStep: step,
})
}
func RequireRoleAndStatus(statuses []string, roles ...string) fiber.Handler {
return AuthMiddleware(AuthConfig{
RequiredRoles: roles,
RequiredStatuses: statuses,
})
}
func GetUserFromContext(c *fiber.Ctx) (*utils.JWTClaims, error) {
claims, ok := c.Locals("user").(*utils.JWTClaims)
if !ok {
return nil, ErrUserContextNotFound
}
return claims, nil
}
func GetAuthContextFromContext(c *fiber.Ctx) (*AuthContext, error) {
authCtx, ok := c.Locals("auth_context").(*AuthContext)
if !ok {
claims, err := GetUserFromContext(c)
if err != nil {
return nil, err
}
return createAuthContext(claims), nil
}
return authCtx, nil
}
func MustGetUserFromContext(c *fiber.Ctx) *utils.JWTClaims {
claims, err := GetUserFromContext(c)
if err != nil {
panic("user context not found")
}
return claims
}
func GetUserID(c *fiber.Ctx) (string, error) {
claims, err := GetUserFromContext(c)
if err != nil {
return "", err
}
return claims.UserID, nil
}
func GetUserRole(c *fiber.Ctx) (string, error) {
claims, err := GetUserFromContext(c)
if err != nil {
return "", err
}
return claims.Role, nil
}
func IsAdmin(c *fiber.Ctx) bool {
claims, err := GetUserFromContext(c)
if err != nil {
return false
}
return claims.Role == utils.RoleAdministrator
}
func IsRegistrationComplete(c *fiber.Ctx) bool {
claims, err := GetUserFromContext(c)
if err != nil {
return false
}
return utils.IsRegistrationComplete(claims.RegistrationStatus)
}
func HasRole(c *fiber.Ctx, role string) bool {
claims, err := GetUserFromContext(c)
if err != nil {
return false
}
return claims.Role == role
}
func HasAnyRole(c *fiber.Ctx, roles ...string) bool {
claims, err := GetUserFromContext(c)
if err != nil {
return false
}
return contains(roles, claims.Role)
}
type RateLimitConfig struct {
MaxRequests int
Window time.Duration
KeyFunc func(*fiber.Ctx) string
SkipFunc func(*fiber.Ctx) bool
}
func AuthRateLimit(config RateLimitConfig) fiber.Handler {
if config.KeyFunc == nil {
config.KeyFunc = func(c *fiber.Ctx) string {
claims, err := GetUserFromContext(c)
if err != nil {
return c.IP()
}
return fmt.Sprintf("user:%s", claims.UserID)
}
}
return func(c *fiber.Ctx) error {
if config.SkipFunc != nil && config.SkipFunc(c) {
return c.Next()
}
key := fmt.Sprintf("rate_limit:%s", config.KeyFunc(c))
var count int
err := utils.GetCache(key, &count)
if err != nil {
count = 0
}
if count >= config.MaxRequests {
return c.Status(fiber.StatusTooManyRequests).JSON(&AuthError{
Code: "RATE_LIMIT_EXCEEDED",
Message: "Terlalu banyak permintaan, coba lagi nanti",
Details: fiber.Map{
"max_requests": config.MaxRequests,
"window": config.Window.String(),
},
})
}
count++
utils.SetCache(key, count, config.Window)
return c.Next()
}
}
func DeviceValidation() fiber.Handler {
return func(c *fiber.Ctx) error {
claims, err := GetUserFromContext(c)
if err != nil {
return err
}
deviceID := c.Get("X-Device-ID")
if deviceID == "" {
return c.Status(fiber.StatusBadRequest).JSON(&AuthError{
Code: "MISSING_DEVICE_ID",
Message: "Device ID diperlukan",
})
}
if subtle.ConstantTimeCompare([]byte(claims.DeviceID), []byte(deviceID)) != 1 {
return c.Status(fiber.StatusForbidden).JSON(&AuthError{
Code: "DEVICE_MISMATCH",
Message: "Device tidak cocok dengan token",
})
}
return c.Next()
}
}
func SessionValidation() fiber.Handler {
return func(c *fiber.Ctx) error {
claims, err := GetUserFromContext(c)
if err != nil {
return err
}
sessionKey := fmt.Sprintf("session:%s", claims.SessionID)
var sessionData interface{}
err = utils.GetCache(sessionKey, &sessionData)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(&AuthError{
Code: "SESSION_EXPIRED",
Message: "Sesi telah berakhir, silakan login kembali",
})
}
return c.Next()
}
}
func AuthLogger() fiber.Handler {
return logger.New(logger.Config{
Format: "[${time}] ${status} - ${method} ${path} - User: ${locals:user_id} Role: ${locals:user_role} IP: ${ip}\n",
CustomTags: map[string]logger.LogFunc{
"user_id": func(output logger.Buffer, c *fiber.Ctx, data *logger.Data, extraParam string) (int, error) {
if claims, err := GetUserFromContext(c); err == nil {
return output.WriteString(claims.UserID)
}
return output.WriteString("anonymous")
},
"user_role": func(output logger.Buffer, c *fiber.Ctx, data *logger.Data, extraParam string) (int, error) {
if claims, err := GetUserFromContext(c); err == nil {
return output.WriteString(claims.Role)
}
return output.WriteString("none")
},
},
})
}

View File

@ -1,29 +0,0 @@
package middleware
import (
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
func RoleMiddleware(allowedRoles ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
if len(allowedRoles) == 0 {
return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: No roles specified")
}
roleID, ok := c.Locals("roleID").(string)
if !ok || roleID == "" {
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Role not found")
}
for _, role := range allowedRoles {
if role == roleID {
return c.Next()
}
}
return utils.GenericResponse(c, fiber.StatusForbidden, "Access Denied: You don't have permission to access this resource")
}
}

View File

@ -11,7 +11,7 @@ type CompanyProfile struct {
CompanyName string `gorm:"not null" json:"company_name"` CompanyName string `gorm:"not null" json:"company_name"`
CompanyAddress string `gorm:"not null" json:"company_address"` CompanyAddress string `gorm:"not null" json:"company_address"`
CompanyPhone string `gorm:"not null" json:"company_phone"` CompanyPhone string `gorm:"not null" json:"company_phone"`
CompanyEmail string `gorm:"not null" json:"company_email"` CompanyEmail string `json:"company_email,omitempty"`
CompanyLogo string `json:"company_logo,omitempty"` CompanyLogo string `json:"company_logo,omitempty"`
CompanyWebsite string `json:"company_website,omitempty"` CompanyWebsite string `json:"company_website,omitempty"`
TaxID string `json:"tax_id,omitempty"` TaxID string `json:"tax_id,omitempty"`

View File

@ -11,9 +11,13 @@ type IdentityCard struct {
Dateofbirth string `gorm:"not null" json:"dateofbirth"` Dateofbirth string `gorm:"not null" json:"dateofbirth"`
Gender string `gorm:"not null" json:"gender"` Gender string `gorm:"not null" json:"gender"`
BloodType string `gorm:"not null" json:"bloodtype"` BloodType string `gorm:"not null" json:"bloodtype"`
Province string `gorm:"not null" json:"province"`
District string `gorm:"not null" json:"district"` District string `gorm:"not null" json:"district"`
SubDistrict string `gorm:"not null" json:"subdistrict"`
Hamlet string `gorm:"not null" json:"hamlet"`
Village string `gorm:"not null" json:"village"` Village string `gorm:"not null" json:"village"`
Neighbourhood string `gorm:"not null" json:"neighbourhood"` Neighbourhood string `gorm:"not null" json:"neighbourhood"`
PostalCode string `gorm:"not null" json:"postalcode"`
Religion string `gorm:"not null" json:"religion"` Religion string `gorm:"not null" json:"religion"`
Maritalstatus string `gorm:"not null" json:"maritalstatus"` Maritalstatus string `gorm:"not null" json:"maritalstatus"`
Job string `gorm:"not null" json:"job"` Job string `gorm:"not null" json:"job"`

View File

@ -4,19 +4,21 @@ import "time"
type TrashCategory struct { type TrashCategory struct {
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"`
Name string `gorm:"not null" json:"name"` Name string `gorm:"not null" json:"trash_name"`
Icon string `json:"icon,omitempty"` IconTrash string `json:"trash_icon,omitempty"`
EstimatedPrice float64 `gorm:"not null" json:"estimated_price"` EstimatedPrice float64 `gorm:"not null" json:"estimated_price"`
Details []TrashDetail `gorm:"foreignKey:CategoryID;constraint:OnDelete:CASCADE;" json:"details"` Variety string `gorm:"not null" json:"variety"`
Details []TrashDetail `gorm:"foreignKey:TrashCategoryID;constraint:OnDelete:CASCADE;" json:"trash_detail"`
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"`
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
} }
type TrashDetail struct { type TrashDetail struct {
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"trashdetail_id"`
CategoryID string `gorm:"type:uuid;not null" json:"category_id"` TrashCategoryID string `gorm:"type:uuid;not null" json:"category_id"`
Description string `gorm:"not null" json:"description"` IconTrashDetail string `json:"trashdetail_icon,omitempty"`
Price float64 `gorm:"not null" json:"price"` Description string `gorm:"not null" json:"description"`
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` StepOrder int `gorm:"not null" json:"step_order"`
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"`
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
} }

View File

@ -3,19 +3,20 @@ package model
import "time" import "time"
type User struct { type User struct {
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"`
Avatar *string `json:"avatar,omitempty"` Avatar *string `json:"avatar,omitempty"`
Name string `gorm:"not null" json:"name"` Name string `gorm:"not null" json:"name"`
Gender string `gorm:"not null" json:"gender"` Gender string `gorm:"not null" json:"gender"`
Dateofbirth string `gorm:"not null" json:"dateofbirth"` Dateofbirth string `gorm:"not null" json:"dateofbirth"`
Placeofbirth string `gorm:"not null" json:"placeofbirth"` Placeofbirth string `gorm:"not null" json:"placeofbirth"`
Phone string `gorm:"not null" json:"phone"` Phone string `gorm:"not null;index" json:"phone"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
PhoneVerified bool `gorm:"default:false" json:"phoneVerified"` PhoneVerified bool `gorm:"default:false" json:"phoneVerified"`
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
RoleID string `gorm:"not null" json:"roleId"` RoleID string `gorm:"not null" json:"roleId"`
Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"` Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"`
RegistrationStatus string `gorm:"default:uncompleted" json:"registrationstatus"` RegistrationStatus string `gorm:"default:uncompleted" json:"registrationstatus"`
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` RegistrationProgress int8 `json:"registration_progress"`
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"`
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
} }

View File

@ -5,6 +5,7 @@ import "time"
type UserPin struct { type UserPin struct {
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"`
UserID string `gorm:"not null" json:"userId"` UserID string `gorm:"not null" json:"userId"`
User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"-"`
Pin string `gorm:"not null" json:"pin"` Pin string `gorm:"not null" json:"pin"`
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"`
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`

View File

@ -2,7 +2,7 @@ package presentation
import ( import (
"rijig/config" "rijig/config"
"rijig/internal/handler" "rijig/internal/about"
"rijig/internal/repositories" "rijig/internal/repositories"
"rijig/internal/services" "rijig/internal/services"
"rijig/middleware" "rijig/middleware"
@ -14,22 +14,22 @@ import (
func AboutRouter(api fiber.Router) { func AboutRouter(api fiber.Router) {
aboutRepo := repositories.NewAboutRepository(config.DB) aboutRepo := repositories.NewAboutRepository(config.DB)
aboutService := services.NewAboutService(aboutRepo) aboutService := services.NewAboutService(aboutRepo)
aboutHandler := handler.NewAboutHandler(aboutService) aboutHandler := about.NewAboutHandler(aboutService)
aboutRoutes := api.Group("/about") aboutRoutes := api.Group("/about")
aboutRoutes.Use(middleware.AuthMiddleware) aboutRoutes.Use(middleware.AuthMiddleware())
aboutRoutes.Get("/", aboutHandler.GetAllAbout) aboutRoutes.Get("/", aboutHandler.GetAllAbout)
aboutRoutes.Get("/:id", aboutHandler.GetAboutByID) aboutRoutes.Get("/:id", aboutHandler.GetAboutByID)
aboutRoutes.Post("/", aboutHandler.CreateAbout) // admin aboutRoutes.Post("/", aboutHandler.CreateAbout) // admin
aboutRoutes.Put("/:id", middleware.RoleMiddleware(utils.RoleAdministrator), aboutHandler.UpdateAbout) aboutRoutes.Put("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.UpdateAbout)
aboutRoutes.Delete("/:id", aboutHandler.DeleteAbout) // admin aboutRoutes.Delete("/:id", aboutHandler.DeleteAbout) // admin
aboutDetailRoutes := api.Group("/about-detail") aboutDetailRoutes := api.Group("/about-detail")
aboutDetailRoutes.Use(middleware.AuthMiddleware) aboutDetailRoutes.Use(middleware.AuthMiddleware())
aboutDetailRoute := api.Group("/about-detail") aboutDetailRoute := api.Group("/about-detail")
aboutDetailRoute.Get("/:id", aboutHandler.GetAboutDetailById) aboutDetailRoute.Get("/:id", aboutHandler.GetAboutDetailById)
aboutDetailRoutes.Post("/", aboutHandler.CreateAboutDetail) // admin aboutDetailRoutes.Post("/", aboutHandler.CreateAboutDetail) // admin
aboutDetailRoutes.Put("/:id", middleware.RoleMiddleware(utils.RoleAdministrator), aboutHandler.UpdateAboutDetail) aboutDetailRoutes.Put("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.UpdateAboutDetail)
aboutDetailRoutes.Delete("/:id", middleware.RoleMiddleware(utils.RoleAdministrator), aboutHandler.DeleteAboutDetail) aboutDetailRoutes.Delete("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.DeleteAboutDetail)
} }

View File

@ -18,9 +18,9 @@ func AddressRouter(api fiber.Router) {
adddressAPI := api.Group("/user/address") adddressAPI := api.Group("/user/address")
adddressAPI.Post("/create-address", middleware.AuthMiddleware, addressHandler.CreateAddress) adddressAPI.Post("/create-address", middleware.AuthMiddleware(), addressHandler.CreateAddress)
adddressAPI.Get("/get-address", middleware.AuthMiddleware, addressHandler.GetAddressByUserID) adddressAPI.Get("/get-address", middleware.AuthMiddleware(), addressHandler.GetAddressByUserID)
adddressAPI.Get("/get-address/:address_id", middleware.AuthMiddleware, addressHandler.GetAddressByID) adddressAPI.Get("/get-address/:address_id", middleware.AuthMiddleware(), addressHandler.GetAddressByID)
adddressAPI.Put("/update-address/:address_id", middleware.AuthMiddleware, addressHandler.UpdateAddress) adddressAPI.Put("/update-address/:address_id", middleware.AuthMiddleware(), addressHandler.UpdateAddress)
adddressAPI.Delete("/delete-address/:address_id", middleware.AuthMiddleware, addressHandler.DeleteAddress) adddressAPI.Delete("/delete-address/:address_id", middleware.AuthMiddleware(), addressHandler.DeleteAddress)
} }

Some files were not shown because too many files have changed in this diff Show More