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=
|
||||
|
||||
# TTL
|
||||
ACCESS_TOKEN_EXPIRY=
|
||||
REFRESH_TOKEN_EXPIRY=
|
||||
PARTIAL_TOKEN_EXPIRY=
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package dto
|
||||
|
||||
import (
|
||||
"rijig/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -45,12 +46,8 @@ func (r *RequestCompanyProfileDTO) ValidateCompanyProfileInput() (map[string][]s
|
|||
errors["company_Address"] = append(errors["company_address"], "Company address is required")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(r.CompanyPhone) == "" {
|
||||
errors["company_Phone"] = append(errors["company_phone"], "Company phone is required")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(r.CompanyEmail) == "" {
|
||||
errors["company_Email"] = append(errors["company_email"], "Company email 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) == "" {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
package dto
|
||||
|
||||
/*
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
@ -62,3 +62,4 @@ func (r *RequestTrashDetailDTO) ValidateTrashDetailInput() (map[string][]string,
|
|||
|
||||
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
|
||||
|
||||
/*
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
@ -24,7 +24,7 @@ func (h *AboutHandler) CreateAbout(c *fiber.Ctx) error {
|
|||
var request dto.RequestAboutDTO
|
||||
if err := c.BodyParser(&request); err != nil {
|
||||
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")
|
||||
|
@ -173,3 +173,4 @@ func (h *AboutHandler) DeleteAboutDetail(c *fiber.Ctx) error {
|
|||
|
||||
return utils.GenericResponse(c, fiber.StatusOK, "Successfully deleted AboutDetail")
|
||||
}
|
||||
*/
|
|
@ -1,5 +1,5 @@
|
|||
package handler
|
||||
|
||||
/*
|
||||
import (
|
||||
"log"
|
||||
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")
|
||||
}
|
||||
|
||||
*/
|
|
@ -1,5 +1,5 @@
|
|||
package handler
|
||||
|
||||
/*
|
||||
import (
|
||||
"log"
|
||||
"rijig/dto"
|
||||
|
@ -79,3 +79,4 @@ func (h *AuthMasyarakatHandler) LogoutHandler(c *fiber.Ctx) error {
|
|||
|
||||
return utils.SuccessResponse(c, nil, "Logged out successfully")
|
||||
}
|
||||
*/
|
|
@ -1,5 +1,5 @@
|
|||
package handler
|
||||
|
||||
/*
|
||||
import (
|
||||
"log"
|
||||
"rijig/dto"
|
||||
|
@ -79,3 +79,4 @@ func (h *AuthPengepulHandler) LogoutHandler(c *fiber.Ctx) error {
|
|||
|
||||
return utils.SuccessResponse(c, nil, "Logged out successfully")
|
||||
}
|
||||
*/
|
|
@ -1,5 +1,5 @@
|
|||
package handler
|
||||
|
||||
/*
|
||||
import (
|
||||
"log"
|
||||
"rijig/dto"
|
||||
|
@ -79,3 +79,4 @@ func (h *AuthPengelolaHandler) LogoutHandler(c *fiber.Ctx) error {
|
|||
|
||||
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")
|
||||
// }
|
||||
|
||||
roles, err := h.RoleService.GetRoles()
|
||||
roles, err := h.RoleService.GetRoles(c.Context())
|
||||
if err != nil {
|
||||
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")
|
||||
// }
|
||||
|
||||
role, err := h.RoleService.GetRoleByID(roleID)
|
||||
role, err := h.RoleService.GetRoleByID(c.Context(), roleID)
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"rijig/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RoleRepository interface {
|
||||
FindByID(id string) (*model.Role, error)
|
||||
FindRoleByName(roleName string) (*model.Role, error)
|
||||
FindAll() ([]model.Role, error)
|
||||
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
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
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
|
||||
err := r.DB.Where("id = ?", id).First(&role).Error
|
||||
err := r.db.WithContext(ctx).Where("id = ?", id).First(&role).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
err := r.DB.Find(&roles).Error
|
||||
err := r.db.WithContext(ctx).Find(&roles).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// spesial code
|
||||
func (r *trashRepository) GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) {
|
||||
var trash model.TrashCategory
|
||||
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
|
||||
|
||||
/*
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -189,3 +189,4 @@ func (s *authAdminService) LogoutAdmin(userID, deviceID string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
*/
|
|
@ -1,5 +1,5 @@
|
|||
package service
|
||||
|
||||
/*
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -168,3 +168,4 @@ func (s *authMasyarakatService) Logout(userID, deviceID string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
*/
|
|
@ -1,5 +1,5 @@
|
|||
package service
|
||||
|
||||
/*
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -168,3 +168,4 @@ func (s *authPengelolaService) Logout(userID, deviceID string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
*/
|
|
@ -1,5 +1,5 @@
|
|||
package service
|
||||
|
||||
/*
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -169,3 +169,4 @@ func (s *authPengepulService) Logout(userID, deviceID string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
*/
|
|
@ -1,6 +1,7 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
|
@ -10,8 +11,8 @@ import (
|
|||
)
|
||||
|
||||
type RoleService interface {
|
||||
GetRoles() ([]dto.RoleResponseDTO, error)
|
||||
GetRoleByID(roleID string) (*dto.RoleResponseDTO, error)
|
||||
GetRoles(ctx context.Context) ([]dto.RoleResponseDTO, error)
|
||||
GetRoleByID(ctx context.Context, roleID string) (*dto.RoleResponseDTO, error)
|
||||
}
|
||||
|
||||
type roleService struct {
|
||||
|
@ -22,8 +23,7 @@ func NewRoleService(roleRepo repositories.RoleRepository) RoleService {
|
|||
return &roleService{RoleRepo: roleRepo}
|
||||
}
|
||||
|
||||
func (s *roleService) GetRoles() ([]dto.RoleResponseDTO, error) {
|
||||
|
||||
func (s *roleService) GetRoles(ctx context.Context) ([]dto.RoleResponseDTO, error) {
|
||||
cacheKey := "roles_list"
|
||||
cachedData, err := utils.GetJSONData(cacheKey)
|
||||
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 {
|
||||
return nil, fmt.Errorf("failed to fetch roles: %v", err)
|
||||
}
|
||||
|
@ -73,9 +73,9 @@ func (s *roleService) GetRoles() ([]dto.RoleResponseDTO, error) {
|
|||
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 {
|
||||
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 {
|
||||
apiKey := c.Get("x-api-key")
|
||||
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")
|
||||
if apiKey != validAPIKey {
|
||||
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid API key")
|
||||
return utils.Unauthorized(c, "Unauthorized: Invalid API key")
|
||||
}
|
||||
|
||||
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"`
|
||||
CompanyAddress string `gorm:"not null" json:"company_address"`
|
||||
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"`
|
||||
CompanyWebsite string `json:"company_website,omitempty"`
|
||||
TaxID string `json:"tax_id,omitempty"`
|
||||
|
|
|
@ -11,9 +11,13 @@ type IdentityCard struct {
|
|||
Dateofbirth string `gorm:"not null" json:"dateofbirth"`
|
||||
Gender string `gorm:"not null" json:"gender"`
|
||||
BloodType string `gorm:"not null" json:"bloodtype"`
|
||||
Province string `gorm:"not null" json:"province"`
|
||||
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"`
|
||||
Neighbourhood string `gorm:"not null" json:"neighbourhood"`
|
||||
PostalCode string `gorm:"not null" json:"postalcode"`
|
||||
Religion string `gorm:"not null" json:"religion"`
|
||||
Maritalstatus string `gorm:"not null" json:"maritalstatus"`
|
||||
Job string `gorm:"not null" json:"job"`
|
||||
|
|
|
@ -4,19 +4,21 @@ import "time"
|
|||
|
||||
type TrashCategory struct {
|
||||
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Name string `gorm:"not null" json:"trash_name"`
|
||||
IconTrash string `json:"trash_icon,omitempty"`
|
||||
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"`
|
||||
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
|
||||
}
|
||||
|
||||
type TrashDetail struct {
|
||||
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"`
|
||||
CategoryID string `gorm:"type:uuid;not null" json:"category_id"`
|
||||
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"trashdetail_id"`
|
||||
TrashCategoryID string `gorm:"type:uuid;not null" json:"category_id"`
|
||||
IconTrashDetail string `json:"trashdetail_icon,omitempty"`
|
||||
Description string `gorm:"not null" json:"description"`
|
||||
Price float64 `gorm:"not null" json:"price"`
|
||||
StepOrder int `gorm:"not null" json:"step_order"`
|
||||
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
|
||||
}
|
||||
|
|
|
@ -9,13 +9,14 @@ type User struct {
|
|||
Gender string `gorm:"not null" json:"gender"`
|
||||
Dateofbirth string `gorm:"not null" json:"dateofbirth"`
|
||||
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"`
|
||||
PhoneVerified bool `gorm:"default:false" json:"phoneVerified"`
|
||||
Password string `json:"password,omitempty"`
|
||||
RoleID string `gorm:"not null" json:"roleId"`
|
||||
Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"`
|
||||
RegistrationStatus string `gorm:"default:uncompleted" json:"registrationstatus"`
|
||||
RegistrationProgress int8 `json:"registration_progress"`
|
||||
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 {
|
||||
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"`
|
||||
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"`
|
||||
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
|
||||
|
|
|
@ -2,7 +2,7 @@ package presentation
|
|||
|
||||
import (
|
||||
"rijig/config"
|
||||
"rijig/internal/handler"
|
||||
"rijig/internal/about"
|
||||
"rijig/internal/repositories"
|
||||
"rijig/internal/services"
|
||||
"rijig/middleware"
|
||||
|
@ -14,22 +14,22 @@ import (
|
|||
func AboutRouter(api fiber.Router) {
|
||||
aboutRepo := repositories.NewAboutRepository(config.DB)
|
||||
aboutService := services.NewAboutService(aboutRepo)
|
||||
aboutHandler := handler.NewAboutHandler(aboutService)
|
||||
aboutHandler := about.NewAboutHandler(aboutService)
|
||||
|
||||
aboutRoutes := api.Group("/about")
|
||||
aboutRoutes.Use(middleware.AuthMiddleware)
|
||||
aboutRoutes.Use(middleware.AuthMiddleware())
|
||||
|
||||
aboutRoutes.Get("/", aboutHandler.GetAllAbout)
|
||||
aboutRoutes.Get("/:id", aboutHandler.GetAboutByID)
|
||||
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
|
||||
|
||||
aboutDetailRoutes := api.Group("/about-detail")
|
||||
aboutDetailRoutes.Use(middleware.AuthMiddleware)
|
||||
aboutDetailRoutes.Use(middleware.AuthMiddleware())
|
||||
aboutDetailRoute := api.Group("/about-detail")
|
||||
aboutDetailRoute.Get("/:id", aboutHandler.GetAboutDetailById)
|
||||
aboutDetailRoutes.Post("/", aboutHandler.CreateAboutDetail) // admin
|
||||
aboutDetailRoutes.Put("/:id", middleware.RoleMiddleware(utils.RoleAdministrator), aboutHandler.UpdateAboutDetail)
|
||||
aboutDetailRoutes.Delete("/:id", middleware.RoleMiddleware(utils.RoleAdministrator), aboutHandler.DeleteAboutDetail)
|
||||
aboutDetailRoutes.Put("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.UpdateAboutDetail)
|
||||
aboutDetailRoutes.Delete("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.DeleteAboutDetail)
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@ func AddressRouter(api fiber.Router) {
|
|||
|
||||
adddressAPI := api.Group("/user/address")
|
||||
|
||||
adddressAPI.Post("/create-address", middleware.AuthMiddleware, addressHandler.CreateAddress)
|
||||
adddressAPI.Get("/get-address", middleware.AuthMiddleware, addressHandler.GetAddressByUserID)
|
||||
adddressAPI.Get("/get-address/:address_id", middleware.AuthMiddleware, addressHandler.GetAddressByID)
|
||||
adddressAPI.Put("/update-address/:address_id", middleware.AuthMiddleware, addressHandler.UpdateAddress)
|
||||
adddressAPI.Delete("/delete-address/:address_id", middleware.AuthMiddleware, addressHandler.DeleteAddress)
|
||||
adddressAPI.Post("/create-address", middleware.AuthMiddleware(), addressHandler.CreateAddress)
|
||||
adddressAPI.Get("/get-address", middleware.AuthMiddleware(), addressHandler.GetAddressByUserID)
|
||||
adddressAPI.Get("/get-address/:address_id", middleware.AuthMiddleware(), addressHandler.GetAddressByID)
|
||||
adddressAPI.Put("/update-address/:address_id", middleware.AuthMiddleware(), addressHandler.UpdateAddress)
|
||||
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