refact: modularization folder struct and fixing auth
This commit is contained in:
parent
58f843cac2
commit
e7c0675f8a
|
@ -23,3 +23,8 @@ API_KEY=
|
||||||
|
|
||||||
#SECRET_KEY
|
#SECRET_KEY
|
||||||
SECRET_KEY=
|
SECRET_KEY=
|
||||||
|
|
||||||
|
# TTL
|
||||||
|
ACCESS_TOKEN_EXPIRY=
|
||||||
|
REFRESH_TOKEN_EXPIRY=
|
||||||
|
PARTIAL_TOKEN_EXPIRY=
|
||||||
|
|
|
@ -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) == "" {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
*/
|
|
@ -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"`
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package about
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package address
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package address
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package cart
|
|
@ -0,0 +1 @@
|
||||||
|
package cart
|
|
@ -0,0 +1 @@
|
||||||
|
package cart
|
|
@ -0,0 +1 @@
|
||||||
|
package cart
|
|
@ -0,0 +1 @@
|
||||||
|
package cart
|
|
@ -0,0 +1 @@
|
||||||
|
package collector
|
|
@ -0,0 +1 @@
|
||||||
|
package collector
|
|
@ -0,0 +1 @@
|
||||||
|
package collector
|
|
@ -0,0 +1 @@
|
||||||
|
package collector
|
|
@ -0,0 +1 @@
|
||||||
|
package collector
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
*/
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
*/
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
*/
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
*/
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
package requestpickup
|
|
@ -0,0 +1 @@
|
||||||
|
package requestpickup
|
|
@ -0,0 +1 @@
|
||||||
|
package requestpickup
|
|
@ -0,0 +1 @@
|
||||||
|
package requestpickup
|
|
@ -0,0 +1 @@
|
||||||
|
package requestpickup
|
|
@ -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"`
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
*/
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
*/
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
*/
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
*/
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package trash
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package userprofile
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package userprofile
|
|
@ -0,0 +1 @@
|
||||||
|
package userprofile
|
|
@ -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")
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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"`
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package wilayahindo
|
|
@ -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(®encies).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(®ency).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 ®ency, 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
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package wilayahindo
|
|
@ -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 ®encyDTO, 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
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue