diff --git a/Dockerfile.dev b/Dockerfile.dev index c08cd9d..3c66d10 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,28 +1,49 @@ -# Dockerfile untuk development environment dengan Air hot reload -FROM golang:1.23-alpine +# Multi-stage Dockerfile untuk development dengan Air hot reload +FROM golang:1.23-alpine AS base -# Install dependencies dan Air -RUN apk add --no-cache git ca-certificates curl && \ - go install github.com/cosmtrek/air@latest +# Install dependencies yang diperlukan +RUN apk add --no-cache \ + git \ + ca-certificates \ + curl \ + tzdata \ + make \ + gcc \ + musl-dev + +# Install Air untuk hot reload dengan versi yang stabil +RUN go install github.com/cosmtrek/air@v1.49.0 + +# Set timezone ke Asia/Jakarta +ENV TZ=Asia/Jakarta +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # Set working directory WORKDIR /app -# Copy go mod files dan download dependencies +# Create user untuk security (non-root) +RUN addgroup -g 1001 -S golang && \ + adduser -S golang -u 1001 -G golang + +# Copy go.mod dan go.sum terlebih dahulu untuk better caching COPY go.mod go.sum ./ -RUN go mod download + +# Download dependencies +RUN go mod download && go mod verify # Copy source code COPY . . -# Create tmp directory untuk Air -RUN mkdir -p tmp +# Create tmp directory dengan permissions yang tepat +RUN mkdir -p tmp && \ + chown -R golang:golang /app && \ + chmod -R 755 /app -# Set timezone (optional) -RUN cp /usr/share/zoneinfo/Asia/Jakarta /etc/localtime +# Switch to non-root user +USER golang # Expose port EXPOSE 7000 -# Run Air untuk hot reload +# Command untuk menjalankan Air CMD ["air", "-c", ".air.toml"] \ No newline at end of file diff --git a/Makefile b/Makefile index 44d98ca..9c7d8d3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Makefile untuk mengelola Docker commands +# Makefile untuk mengelola Docker commands - Optimized Version .PHONY: help build up down restart logs clean dev prod dev-build dev-up dev-down dev-logs @@ -6,162 +6,146 @@ GREEN := \033[0;32m YELLOW := \033[1;33m RED := \033[0;31m +BLUE := \033[0;34m +PURPLE := \033[0;35m +CYAN := \033[0;36m NC := \033[0m # No Color +# Project variables +PROJECT_NAME := rijig_backend +DEV_COMPOSE_FILE := docker-compose.dev.yml + # Default target help: - @echo "$(GREEN)Available commands:$(NC)" - @echo "$(YELLOW)Production:$(NC)" - @echo " build - Build all Docker images" - @echo " up - Start all services" - @echo " down - Stop all services" - @echo " restart - Restart all services" - @echo " logs - Show logs for all services" - @echo " clean - Remove all containers and volumes" - @echo " prod - Start production environment" - @echo " psql-prod - Execute psql in production postgres" - @echo " redis-prod - Execute redis-cli in production redis" + @echo "$(GREEN)๐Ÿš€ $(PROJECT_NAME) - Available Commands:$(NC)" @echo "" - @echo "$(YELLOW)Development (dengan Air hot reload):$(NC)" - @echo " dev-build - Build development images" - @echo " dev-up - Start development environment dengan hot reload" - @echo " dev-down - Stop development environment" - @echo " dev-logs - Show development logs" - @echo " dev-clean - Clean development environment" - @echo " dev-restart- Restart development environment" - @echo " psql - Execute psql in development postgres" - @echo " redis-cli - Execute redis-cli in development redis" + @echo "$(YELLOW)๐Ÿ“ฆ Development Commands (Hot Reload):$(NC)" + @echo " $(CYAN)dev$(NC) - Complete development setup (build + up)" + @echo " $(CYAN)dev-build$(NC) - Build development images" + @echo " $(CYAN)dev-up$(NC) - Start development environment" + @echo " $(CYAN)dev-down$(NC) - Stop development environment" + @echo " $(CYAN)dev-restart$(NC) - Restart development services" + @echo " $(CYAN)dev-logs$(NC) - Show development logs (all services)" + @echo " $(CYAN)dev-clean$(NC) - Clean development environment" @echo "" - @echo "$(YELLOW)Utilities:$(NC)" - @echo " app-logs - Show only app logs" - @echo " db-logs - Show only database logs" - @echo " status - Check service status" - @echo " shell - Execute bash in app container" - -# Production Commands -build: - @echo "$(GREEN)Building production images...$(NC)" - docker compose build --no-cache - -up: - @echo "$(GREEN)Starting production services...$(NC)" - docker compose up -d - -down: - @echo "$(RED)Stopping production services...$(NC)" - docker compose down - -restart: - @echo "$(YELLOW)Restarting production services...$(NC)" - docker compose restart - -logs: - @echo "$(GREEN)Showing production logs...$(NC)" - docker compose logs -f - -clean: - @echo "$(RED)Cleaning production environment...$(NC)" - docker compose down -v --remove-orphans - docker system prune -f - docker volume prune -f - -prod: - @echo "$(GREEN)Starting production environment...$(NC)" - docker compose up -d - -# Production utilities -psql-prod: - @echo "$(GREEN)Connecting to production PostgreSQL...$(NC)" - docker compose exec postgres psql -U postgres -d apirijig_v2 - -redis-prod: - @echo "$(GREEN)Connecting to production Redis...$(NC)" - docker compose exec redis redis-cli - -# Development Commands (dengan Air hot reload) -dev-build: - @echo "$(GREEN)Building development images dengan Air...$(NC)" - docker compose -f docker-compose.dev.yml build --no-cache - -dev-up: - @echo "$(GREEN)Starting development environment dengan Air hot reload...$(NC)" - docker compose -f docker-compose.dev.yml up -d - @echo "$(GREEN)Development services started!$(NC)" - @echo "$(YELLOW)API Server: http://localhost:7000$(NC)" - @echo "$(YELLOW)PostgreSQL: localhost:5433$(NC)" - @echo "$(YELLOW)Redis: localhost:6378$(NC)" - @echo "$(YELLOW)pgAdmin: http://localhost:8080 (admin@rijig.com / admin123)$(NC)" - @echo "$(YELLOW)Redis Commander: http://localhost:8081$(NC)" + @echo "$(YELLOW)๐Ÿ› ๏ธ Development Utilities:$(NC)" + @echo " $(CYAN)dev-app-logs$(NC) - Show only app logs" + @echo " $(CYAN)dev-db-logs$(NC) - Show only database logs" + @echo " $(CYAN)dev-shell$(NC) - Access app container shell" + @echo " $(CYAN)dev-status$(NC) - Check development services status" + @echo " $(CYAN)psql$(NC) - Connect to development PostgreSQL" + @echo " $(CYAN)redis-cli$(NC) - Connect to development Redis" @echo "" - @echo "$(GREEN)โœจ Hot reload is active! Edit your Go files and see changes automatically โœจ$(NC)" + @echo "$(YELLOW)๐Ÿงน Maintenance:$(NC)" + @echo " $(RED)clean-all$(NC) - Clean everything (containers, volumes, images)" + @echo " $(RED)system-prune$(NC) - Clean Docker system" + @echo " $(CYAN)stats$(NC) - Show container resource usage" -dev-down: - @echo "$(RED)Stopping development services...$(NC)" - docker compose -f docker-compose.dev.yml down - -dev-logs: - @echo "$(GREEN)Showing development logs...$(NC)" - docker compose -f docker-compose.dev.yml logs -f - -dev-clean: - @echo "$(RED)Cleaning development environment...$(NC)" - docker compose -f docker-compose.dev.yml down -v --remove-orphans - docker system prune -f - -dev-restart: - @echo "$(YELLOW)Restarting development services...$(NC)" - docker compose -f docker-compose.dev.yml restart - -# Development utilities (FIXED - menggunakan -f docker-compose.dev.yml) -dev-app-logs: - @echo "$(GREEN)Showing development app logs...$(NC)" - docker compose -f docker-compose.dev.yml logs -f app - -dev-db-logs: - @echo "$(GREEN)Showing development database logs...$(NC)" - docker compose -f docker-compose.dev.yml logs -f postgres - -dev-shell: - @echo "$(GREEN)Accessing development app container...$(NC)" - docker compose -f docker-compose.dev.yml exec app sh - -dev-status: - @echo "$(GREEN)Development service status:$(NC)" - docker compose -f docker-compose.dev.yml ps - -# FIXED: Development database access (default untuk development) -psql: - @echo "$(GREEN)Connecting to development PostgreSQL...$(NC)" - docker compose -f docker-compose.dev.yml exec postgres psql -U postgres -d apirijig_v2 - -redis-cli: - @echo "$(GREEN)Connecting to development Redis...$(NC)" - docker compose -f docker-compose.dev.yml exec redis redis-cli - -# Shared utilities (default ke production) -app-logs: - docker compose logs -f app - -db-logs: - docker compose logs -f postgres - -status: - docker compose ps - -shell: - docker compose exec app sh - -# Rebuild and restart app only -app-rebuild: - docker compose build app - docker compose up -d app - -# View real-time resource usage -stats: - docker stats +# ====================== +# DEVELOPMENT COMMANDS +# ====================== # Quick development setup (recommended) -dev: - @echo "$(GREEN)Setting up complete development environment...$(NC)" - make dev-build - make dev-up \ No newline at end of file +dev: dev-build dev-up + @echo "$(GREEN)โœจ Development environment ready!$(NC)" + @echo "$(BLUE)๐ŸŒ Services:$(NC)" + @echo " โ€ข API Server: $(CYAN)http://localhost:7000$(NC)" + @echo " โ€ข PostgreSQL: $(CYAN)localhost:5433$(NC)" + @echo " โ€ข Redis: $(CYAN)localhost:6378$(NC)" + @echo " โ€ข pgAdmin: $(CYAN)http://localhost:8080$(NC) (admin@rijig.com / admin123)" + @echo " โ€ข Redis Commander: $(CYAN)http://localhost:8081$(NC)" + @echo "" + @echo "$(GREEN)๐Ÿ”ฅ Hot reload is active! Edit your Go files and see changes automatically$(NC)" + +dev-build: + @echo "$(YELLOW)๐Ÿ”จ Building development images...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) build --no-cache + @echo "$(GREEN)โœ… Development images built successfully!$(NC)" + +dev-up: + @echo "$(YELLOW)๐Ÿš€ Starting development services...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) up -d + @echo "$(GREEN)โœ… Development services started!$(NC)" + @make dev-status + +dev-down: + @echo "$(RED)๐Ÿ›‘ Stopping development services...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) down + @echo "$(GREEN)โœ… Development services stopped!$(NC)" + +dev-restart: + @echo "$(YELLOW)๐Ÿ”„ Restarting development services...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) restart + @echo "$(GREEN)โœ… Development services restarted!$(NC)" + +dev-logs: + @echo "$(CYAN)๐Ÿ“‹ Showing development logs (Ctrl+C to exit)...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) logs -f --tail=100 + +dev-clean: + @echo "$(RED)๐Ÿงน Cleaning development environment...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) down -v --remove-orphans + @echo "$(GREEN)โœ… Development environment cleaned!$(NC)" + +# ====================== +# DEVELOPMENT UTILITIES +# ====================== + +dev-app-logs: + @echo "$(CYAN)๐Ÿ“‹ Showing app logs (Ctrl+C to exit)...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) logs -f --tail=50 app + +dev-db-logs: + @echo "$(CYAN)๐Ÿ“‹ Showing database logs (Ctrl+C to exit)...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) logs -f --tail=50 postgres + +dev-shell: + @echo "$(CYAN)๐Ÿš Accessing app container shell...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) exec app sh + +dev-status: + @echo "$(BLUE)๐Ÿ“Š Development services status:$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) ps + +psql: + @echo "$(CYAN)๐Ÿ˜ Connecting to development PostgreSQL...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) exec postgres psql -U postgres -d apirijig_v2 + +redis-cli: + @echo "$(CYAN)โšก Connecting to development Redis...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) exec redis redis-cli + +# ====================== +# MAINTENANCE COMMANDS +# ====================== + +clean-all: + @echo "$(RED)๐Ÿงน Performing complete cleanup...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) down -v --remove-orphans 2>/dev/null || true + @echo "$(YELLOW)๐Ÿ—‘๏ธ Removing unused containers, networks, and images...$(NC)" + @docker system prune -a -f --volumes + @echo "$(GREEN)โœ… Complete cleanup finished!$(NC)" + +system-prune: + @echo "$(YELLOW)๐Ÿ—‘๏ธ Cleaning Docker system...$(NC)" + @docker system prune -f + @echo "$(GREEN)โœ… Docker system cleaned!$(NC)" + +stats: + @echo "$(BLUE)๐Ÿ“ˆ Container resource usage:$(NC)" + @docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}" + +# ====================== +# QUICK COMMANDS +# ====================== + +# App only restart (faster for development) +app-restart: + @echo "$(YELLOW)๐Ÿ”„ Restarting app container only...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) restart app + @echo "$(GREEN)โœ… App container restarted!$(NC)" + +# Check if containers are healthy +health-check: + @echo "$(BLUE)๐Ÿฅ Checking container health...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) ps --format "table {{.Name}}\t{{.Status}}" \ No newline at end of file diff --git a/config/setup_config.go b/config/setup_config.go index 0702601..fc7b531 100644 --- a/config/setup_config.go +++ b/config/setup_config.go @@ -8,12 +8,9 @@ import ( ) func SetupConfig() { - if _, exists := os.LookupEnv("DOCKER_ENV"); exists { - log.Println("Running in Docker container, using environment variables") } else { - err := godotenv.Load(".env.dev") if err != nil { log.Printf("Warning: Error loading .env file: %v", err) diff --git a/config/whatsapp.go b/config/whatsapp.go index aef3559..9f1372d 100644 --- a/config/whatsapp.go +++ b/config/whatsapp.go @@ -2,16 +2,16 @@ package config import ( "context" + "encoding/base64" "fmt" "log" "os" "os/signal" - "sync" "syscall" - "time" _ "github.com/lib/pq" - "github.com/mdp/qrterminal/v3" + + "github.com/skip2/go-qrcode" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/store/sqlstore" @@ -21,355 +21,156 @@ import ( "google.golang.org/protobuf/proto" ) -type WhatsAppManager struct { - Client *whatsmeow.Client - container *sqlstore.Container - isConnected bool - mu sync.RWMutex - ctx context.Context - cancel context.CancelFunc - shutdownCh chan struct{} +type WhatsAppService struct { + Client *whatsmeow.Client + Container *sqlstore.Container } -var ( - waManager *WhatsAppManager - once sync.Once -) - -func GetWhatsAppManager() *WhatsAppManager { - once.Do(func() { - ctx, cancel := context.WithCancel(context.Background()) - waManager = &WhatsAppManager{ - ctx: ctx, - cancel: cancel, - shutdownCh: make(chan struct{}), - } - }) - return waManager -} +var whatsappService *WhatsAppService func InitWhatsApp() { - manager := GetWhatsAppManager() + var err error - log.Println("Initializing WhatsApp client...") - - if err := manager.setupDatabase(); err != nil { - log.Fatalf("Failed to setup WhatsApp database: %v", err) - } - - if err := manager.setupClient(); err != nil { - log.Fatalf("Failed to setup WhatsApp client: %v", err) - } - - if err := manager.handleAuthentication(); err != nil { - log.Fatalf("Failed to authenticate WhatsApp: %v", err) - } - - manager.setupEventHandlers() - - go manager.handleShutdown() - - log.Println("WhatsApp client initialized successfully and ready to send messages!") -} - -func (w *WhatsAppManager) setupDatabase() error { - dbLog := waLog.Stdout("WhatsApp-DB", "ERROR", true) - - dsn := fmt.Sprintf( - "postgres://%s:%s@%s:%s/%s?sslmode=disable", + connectionString := fmt.Sprintf( + "user=%s password=%s dbname=%s host=%s port=%s sslmode=disable", os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), + os.Getenv("DB_NAME"), os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), - os.Getenv("DB_NAME"), ) - var err error - w.container, err = sqlstore.New("postgres", dsn, dbLog) + dbLog := waLog.Stdout("Database", "DEBUG", true) + container, err := sqlstore.New("postgres", connectionString, dbLog) if err != nil { - return fmt.Errorf("failed to connect to database: %v", err) + log.Fatalf("Failed to connect to WhatsApp database: %v", err) } - log.Println("WhatsApp database connection established") - return nil + whatsappService = &WhatsAppService{ + Container: container, + } } -func (w *WhatsAppManager) setupClient() error { - deviceStore, err := w.container.GetFirstDevice() - if err != nil { - return fmt.Errorf("failed to get device store: %v", err) - } - - clientLog := waLog.Stdout("WhatsApp-Client", "ERROR", true) - w.Client = whatsmeow.NewClient(deviceStore, clientLog) - - return nil +func GetWhatsAppService() *WhatsAppService { + return whatsappService } -func (w *WhatsAppManager) handleAuthentication() error { - if w.Client.Store.ID == nil { - log.Println("WhatsApp client not logged in, generating QR code...") - return w.authenticateWithQR() +func eventHandler(evt interface{}) { + switch v := evt.(type) { + case *events.Message: + fmt.Println("Received a message!", v.Message.GetConversation()) + case *events.Connected: + fmt.Println("WhatsApp client connected!") + case *events.Disconnected: + fmt.Println("WhatsApp client disconnected!") } - - log.Println("WhatsApp client already logged in, connecting...") - return w.connect() } -func (w *WhatsAppManager) authenticateWithQR() error { - qrChan, err := w.Client.GetQRChannel(w.ctx) +func (wa *WhatsAppService) GenerateQR() (string, error) { + if wa.Container == nil { + return "", fmt.Errorf("container is not initialized") + } + + deviceStore, err := wa.Container.GetFirstDevice() if err != nil { - return fmt.Errorf("failed to get QR channel: %v", err) + return "", fmt.Errorf("failed to get first device: %v", err) } - if err := w.Client.Connect(); err != nil { - return fmt.Errorf("failed to connect client: %v", err) - } + clientLog := waLog.Stdout("Client", "DEBUG", true) + wa.Client = whatsmeow.NewClient(deviceStore, clientLog) + wa.Client.AddEventHandler(eventHandler) - qrTimeout := time.NewTimer(3 * time.Minute) - defer qrTimeout.Stop() + if wa.Client.Store.ID == nil { + fmt.Println("Client is not logged in, generating QR code...") + qrChan, _ := wa.Client.GetQRChannel(context.Background()) + err = wa.Client.Connect() + if err != nil { + return "", fmt.Errorf("failed to connect: %v", err) + } - for { - select { - case evt := <-qrChan: - switch evt.Event { - case "code": - fmt.Println("\n=== QR CODE UNTUK LOGIN WHATSAPP ===") - generateQRCode(evt.Code) - fmt.Println("Scan QR code di atas dengan WhatsApp Anda") - fmt.Println("QR code akan expired dalam 3 menit") - case "success": - log.Println("โœ… WhatsApp login successful!") - w.setConnected(true) - return nil - case "timeout": - return fmt.Errorf("QR code expired, please restart") - default: - log.Printf("Login status: %s", evt.Event) + for evt := range qrChan { + if evt.Event == "code" { + fmt.Println("QR code generated:", evt.Code) + png, err := qrcode.Encode(evt.Code, qrcode.Medium, 256) + if err != nil { + return "", fmt.Errorf("failed to create QR code: %v", err) + } + dataURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(png) + return dataURI, nil + } else { + fmt.Println("Login event:", evt.Event) + if evt.Event == "success" { + return "success", nil + } } - case <-qrTimeout.C: - return fmt.Errorf("QR code authentication timeout after 3 minutes") - case <-w.ctx.Done(): - return fmt.Errorf("authentication cancelled") } - } -} - -func (w *WhatsAppManager) connect() error { - if err := w.Client.Connect(); err != nil { - return fmt.Errorf("failed to connect: %v", err) - } - - time.Sleep(2 * time.Second) - w.setConnected(true) - return nil -} - -func (w *WhatsAppManager) setupEventHandlers() { - w.Client.AddEventHandler(func(evt interface{}) { - switch v := evt.(type) { - case *events.Connected: - log.Println("โœ… WhatsApp client connected") - w.setConnected(true) - case *events.Disconnected: - log.Println("โŒ WhatsApp client disconnected") - w.setConnected(false) - case *events.LoggedOut: - log.Println("๐Ÿšช WhatsApp client logged out") - w.setConnected(false) - case *events.Message: - log.Printf("๐Ÿ“จ Message received from %s", v.Info.Sender) + } else { + fmt.Println("Client already logged in, connecting...") + err = wa.Client.Connect() + if err != nil { + return "", fmt.Errorf("failed to connect: %v", err) } - }) + return "already_connected", nil + } + + return "", fmt.Errorf("failed to generate QR code") } -func (w *WhatsAppManager) setConnected(status bool) { - w.mu.Lock() - defer w.mu.Unlock() - w.isConnected = status -} - -func (w *WhatsAppManager) IsConnected() bool { - w.mu.RLock() - defer w.mu.RUnlock() - return w.isConnected -} - -func generateQRCode(qrString string) { - qrterminal.GenerateHalfBlock(qrString, qrterminal.M, os.Stdout) -} - -func (w *WhatsAppManager) handleShutdown() { - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - - select { - case <-sigChan: - log.Println("Received shutdown signal...") - case <-w.ctx.Done(): - log.Println("Context cancelled...") +func (wa *WhatsAppService) SendMessage(phoneNumber, message string) error { + if wa.Client == nil { + return fmt.Errorf("client not initialized") } - w.shutdown() -} - -func (w *WhatsAppManager) shutdown() { - log.Println("Shutting down WhatsApp client...") - - w.cancel() - - if w.Client != nil { - w.Client.Disconnect() - } - - if w.container != nil { - w.container.Close() - } - - close(w.shutdownCh) - log.Println("WhatsApp client shutdown completed") -} - -func SendWhatsAppMessage(phone, message string) error { - manager := GetWhatsAppManager() - - if manager.Client == nil { - return fmt.Errorf("WhatsApp client is not initialized") - } - - if !manager.IsConnected() { - return fmt.Errorf("WhatsApp client is not connected") - } - - if phone == "" || message == "" { - return fmt.Errorf("phone number and message cannot be empty") - } - - if phone[0] == '0' { - phone = "62" + phone[1:] // Convert 08xx menjadi 628xx - } - if phone[:2] != "62" { - phone = "62" + phone // Tambahkan 62 jika belum ada - } - - // Parse JID - targetJID, err := types.ParseJID(phone + "@s.whatsapp.net") + targetJID, err := types.ParseJID(phoneNumber + "@s.whatsapp.net") if err != nil { - return fmt.Errorf("invalid phone number format: %v", err) + return fmt.Errorf("invalid phone number: %v", err) } - // Buat pesan msg := &waE2E.Message{ Conversation: proto.String(message), } - // Kirim dengan timeout - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - resp, err := manager.Client.SendMessage(ctx, targetJID, msg) + _, err = wa.Client.SendMessage(context.Background(), targetJID, msg) if err != nil { return fmt.Errorf("failed to send message: %v", err) } - log.Printf("โœ… Message sent to %s (ID: %s)", phone, resp.ID) return nil } -// SendWhatsAppMessageBatch - Kirim pesan ke multiple nomor -func SendWhatsAppMessageBatch(phoneNumbers []string, message string) []error { - var errors []error +func (wa *WhatsAppService) Logout() error { + if wa.Client == nil { + return fmt.Errorf("no active client session") + } - for _, phone := range phoneNumbers { - if err := SendWhatsAppMessage(phone, message); err != nil { - errors = append(errors, fmt.Errorf("failed to send to %s: %v", phone, err)) - continue + err := wa.Client.Logout() + if err != nil { + return fmt.Errorf("failed to logout: %v", err) + } + + wa.Client.Disconnect() + wa.Client = nil + return nil +} + +func (wa *WhatsAppService) IsConnected() bool { + return wa.Client != nil && wa.Client.IsConnected() +} + +func (wa *WhatsAppService) IsLoggedIn() bool { + return wa.Client != nil && wa.Client.Store.ID != nil +} + +func (wa *WhatsAppService) GracefulShutdown() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + go func() { + <-c + fmt.Println("Shutting down WhatsApp client...") + if wa.Client != nil { + wa.Client.Disconnect() } - - // Delay untuk menghindari rate limit - time.Sleep(1 * time.Second) - } - - return errors -} - -// GetWhatsAppStatus - Cek status koneksi -func GetWhatsAppStatus() map[string]interface{} { - manager := GetWhatsAppManager() - - status := map[string]interface{}{ - "initialized": manager.Client != nil, - "connected": manager.IsConnected(), - "logged_in": false, - "jid": "", - } - - if manager.Client != nil && manager.Client.Store.ID != nil { - status["logged_in"] = true - status["jid"] = manager.Client.Store.ID.String() - } - - return status -} - -// LogoutWhatsApp - Logout dan cleanup -func LogoutWhatsApp() error { - manager := GetWhatsAppManager() - - if manager.Client == nil { - return fmt.Errorf("WhatsApp client is not initialized") - } - - log.Println("Logging out WhatsApp...") - - // Logout - err := manager.Client.Logout() - if err != nil { - log.Printf("Warning: Failed to logout properly: %v", err) - } - - // Disconnect - manager.Client.Disconnect() - manager.setConnected(false) - - // Hapus device dari store - if err := manager.removeDeviceFromStore(); err != nil { - log.Printf("Warning: Failed to remove device: %v", err) - } - - // Close database - if manager.container != nil { - manager.container.Close() - } - - log.Println("โœ… WhatsApp logout completed") - return nil -} - -func (w *WhatsAppManager) removeDeviceFromStore() error { - deviceStore, err := w.container.GetFirstDevice() - if err != nil { - return err - } - - if deviceStore != nil && deviceStore.ID != nil { - return deviceStore.Delete() - } - - return nil -} - -// IsValidPhoneNumber - Validasi format nomor telepon Indonesia -func IsValidPhoneNumber(phone string) bool { - // Minimal validasi untuk nomor Indonesia - if len(phone) < 10 || len(phone) > 15 { - return false - } - - // Cek awalan nomor Indonesia - if phone[:2] == "62" || phone[0] == '0' { - return true - } - - return false + os.Exit(0) + }() } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e152c00..ed90cd2 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -51,44 +51,15 @@ services: restart: unless-stopped ports: - "7000:7000" - environment: - # Docker Environment Flag - DOCKER_ENV: "true" - - # Base URL - BASE_URL: /apirijig/v2 - - # Server Settings - SERVER_HOST: 0.0.0.0 - SERVER_PORT: 7000 - - # Database Settings - menggunakan service name sebagai host - DB_HOST: postgres - DB_PORT: 5432 - DB_NAME: apirijig_v2 - DB_USER: postgres - DB_PASSWORD: pahmiadmin - - # Redis Settings - menggunakan service name sebagai host - REDIS_HOST: redis - REDIS_PORT: 6379 - REDIS_PASSWORD: "" - REDIS_DB: 0 - - # Auth Keys - API_KEY: apirijikL0RH64wfkEpPqjAroLVPuFgT0EpsSLBPsmyUvIqZrUAi6X3HNPM7Vter - SECRET_KEY: TJ6h3vPMPlAuv7cbD27RU1/UyRctEih5k4H3+o7tZM1PSwTcoFETL6lqB54= - - # TTL Settings - ACCESS_TOKEN_EXPIRY: 23*time.Hour - REFRESH_TOKEN_EXPIRY: 28*24*time.Hour - PARTIAL_TOKEN_EXPIRY: 2*time.Hour + env_file: + - .env.docker volumes: # Mount source code untuk hot reload - .:/app - # Exclude node_modules dan vendor (jika ada) + # Cache Go modules untuk performance + - go_modules_cache:/go/pkg/mod + # Exclude tmp directory untuk mencegah konflik - /app/tmp - - /app/vendor depends_on: postgres: condition: service_healthy @@ -96,6 +67,7 @@ services: condition: service_healthy networks: - rijig_network_dev + working_dir: /app # pgAdmin (optional - untuk GUI database management) pgadmin: @@ -137,3 +109,4 @@ volumes: postgres_data_dev: redis_data_dev: pgadmin_data_dev: + go_modules_cache: diff --git a/go.mod b/go.mod index 687dde0..36bd859 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,10 @@ require ( gorm.io/gorm v1.25.12 ) -require ( - golang.org/x/term v0.30.0 // indirect - rsc.io/qr v0.2.0 // indirect -) +// require ( +// golang.org/x/term v0.30.0 // indirect +// rsc.io/qr v0.2.0 // indirect +// ) require ( filippo.io/edwards25519 v1.1.0 // indirect @@ -35,9 +35,9 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mdp/qrterminal/v3 v3.2.0 github.com/rivo/uniseg v0.2.0 // indirect github.com/rs/zerolog v1.33.0 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // direct github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect diff --git a/go.sum b/go.sum index a238c19..58846b8 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,8 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/internal/authentication/authentication_handler.go b/internal/authentication/authentication_handler.go index cc27327..31f83e4 100644 --- a/internal/authentication/authentication_handler.go +++ b/internal/authentication/authentication_handler.go @@ -210,11 +210,6 @@ func (h *AuthenticationHandler) LogoutAuthentication(c *fiber.Ctx) error { 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 { diff --git a/internal/authentication/authentication_service.go b/internal/authentication/authentication_service.go index 8a9dff8..3956216 100644 --- a/internal/authentication/authentication_service.go +++ b/internal/authentication/authentication_service.go @@ -33,6 +33,21 @@ func NewAuthenticationService(authRepo AuthenticationRepository, roleRepo role.R return &authenticationService{authRepo, roleRepo} } +func normalizeRoleName(roleName string) string { + switch strings.ToLower(roleName) { + case "administrator", "admin": + return utils.RoleAdministrator + case "pengelola": + return utils.RolePengelola + case "pengepul": + return utils.RolePengepul + case "masyarakat": + return utils.RoleMasyarakat + default: + return strings.ToLower(roleName) + } +} + func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*AuthResponse, error) { user, err := s.authRepo.FindUserByEmail(ctx, req.Email) if err != nil { @@ -103,14 +118,16 @@ func (s *authenticationService) RegisterAdmin(ctx context.Context, req *Register func (s *authenticationService) SendRegistrationOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) { - existingUser, err := s.authRepo.FindUserByPhoneAndRole(ctx, req.Phone, strings.ToLower(req.RoleName)) + normalizedRole := strings.ToLower(req.RoleName) + + existingUser, err := s.authRepo.FindUserByPhoneAndRole(ctx, req.Phone, normalizedRole) 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) + roleData, err := s.roleRepo.FindRoleByName(ctx, normalizedRole) if err != nil { - return nil, fmt.Errorf("role tidak valid") + return nil, fmt.Errorf("role tidak valid: %v", err) } rateLimitKey := fmt.Sprintf("otp_limit:%s", req.Phone) @@ -125,33 +142,34 @@ func (s *authenticationService) SendRegistrationOTP(ctx context.Context, req *Lo 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, + Phone: req.Phone, + OTP: otp, + Role: normalizedRole, + RoleID: roleData.ID, + Type: "register", + Attempts: 0, + ExpiresAt: time.Now().Add(90 * time.Second), } - err = utils.SetCacheWithTTL(otpKey, otpData, 1*time.Minute) + err = utils.SetCacheWithTTL(otpKey, otpData, 90*time.Second) if err != nil { return nil, fmt.Errorf("gagal menyimpan OTP: %v", err) } - err = sendOTPViaSMS(req.Phone, otp) + err = sendOTP(req.Phone, otp) if err != nil { return nil, fmt.Errorf("gagal mengirim OTP: %v", err) } return &OTPResponse{ Message: "OTP berhasil dikirim", - ExpiresIn: 60, + ExpiresIn: 90, 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) @@ -166,7 +184,7 @@ func (s *authenticationService) VerifyRegistrationOTP(ctx context.Context, req * if otpData.OTP != req.Otp { otpData.Attempts++ - utils.SetCache(otpKey, otpData, time.Until(otpData.ExpiresAt)) + utils.SetCacheWithTTL(otpKey, otpData, time.Until(otpData.ExpiresAt)) return nil, fmt.Errorf("kode OTP salah") } @@ -174,12 +192,14 @@ func (s *authenticationService) VerifyRegistrationOTP(ctx context.Context, req * return nil, fmt.Errorf("role tidak sesuai") } + normalizedRole := strings.ToLower(req.RoleName) + user := &model.User{ Phone: req.Phone, PhoneVerified: true, RoleID: otpData.RoleID, RegistrationStatus: utils.RegStatusIncomplete, - RegistrationProgress: 0, + RegistrationProgress: utils.ProgressOTPVerified, Name: "", Gender: "", Dateofbirth: "", @@ -191,11 +211,15 @@ func (s *authenticationService) VerifyRegistrationOTP(ctx context.Context, req * return nil, fmt.Errorf("gagal membuat user: %v", err) } + if user.ID == "" { + return nil, fmt.Errorf("gagal mendapatkan user ID setelah registrasi") + } + utils.DeleteCache(otpKey) tokenResponse, err := utils.GenerateTokenPair( user.ID, - req.RoleName, + normalizedRole, req.DeviceID, user.RegistrationStatus, int(user.RegistrationProgress), @@ -205,15 +229,18 @@ func (s *authenticationService) VerifyRegistrationOTP(ctx context.Context, req * return nil, fmt.Errorf("gagal generate token: %v", err) } - nextStep := utils.GetNextRegistrationStep(req.RoleName, int(user.RegistrationProgress),user.RegistrationStatus) + nextStep := utils.GetNextRegistrationStep( + normalizedRole, + int(user.RegistrationProgress), + user.RegistrationStatus, + ) return &AuthResponse{ - Message: "Registrasi berhasil", - AccessToken: tokenResponse.AccessToken, - RefreshToken: tokenResponse.RefreshToken, - TokenType: string(tokenResponse.TokenType), - ExpiresIn: tokenResponse.ExpiresIn, - + Message: "Registrasi berhasil", + AccessToken: tokenResponse.AccessToken, + RefreshToken: tokenResponse.RefreshToken, + TokenType: string(tokenResponse.TokenType), + ExpiresIn: tokenResponse.ExpiresIn, RegistrationStatus: user.RegistrationStatus, NextStep: nextStep, SessionID: tokenResponse.SessionID, @@ -256,7 +283,7 @@ func (s *authenticationService) SendLoginOTP(ctx context.Context, req *LoginorRe return nil, fmt.Errorf("gagal menyimpan OTP: %v", err) } - err = sendOTPViaSMS(req.Phone, otp) + err = sendOTP(req.Phone, otp) if err != nil { return nil, fmt.Errorf("gagal mengirim OTP: %v", err) } @@ -349,7 +376,7 @@ func isRateLimited(key string, maxAttempts int, duration time.Duration) bool { return false } -func sendOTPViaSMS(phone, otp string) error { +func sendOTP(phone, otp string) error { fmt.Printf("Sending OTP %s to %s\n", otp, phone) return nil diff --git a/internal/whatsapp/scanner.html b/internal/whatsapp/scanner.html new file mode 100644 index 0000000..160c4e2 --- /dev/null +++ b/internal/whatsapp/scanner.html @@ -0,0 +1,184 @@ + + + + + + WhatsApp QR Scanner + + + +
+ + +

Scan QR code untuk menghubungkan WhatsApp Anda

+ +
+ WhatsApp QR Code +
+ +
+
+

Cara menggunakan:

+
    +
  1. Buka WhatsApp di ponsel Anda
  2. +
  3. Tap Menu atau Settings dan pilih WhatsApp Web
  4. +
  5. Arahkan ponsel Anda ke QR code ini untuk memindainya
  6. +
  7. Tunggu hingga terhubung
  8. +
+
+ +
+ + Menunggu pemindaian QR code... +
+
+
+ + + + \ No newline at end of file diff --git a/internal/whatsapp/success_scan.html b/internal/whatsapp/success_scan.html new file mode 100644 index 0000000..2063fcc --- /dev/null +++ b/internal/whatsapp/success_scan.html @@ -0,0 +1,411 @@ + + + + + + WhatsApp - Berhasil Terhubung + + + +
+ + +
โœ…
+ +
+ WhatsApp berhasil terhubung dan siap digunakan! +
+ +
+
+ Status Koneksi: +
+ + Checking... +
+
+
+ Status Login: +
+ + Checking... +
+
+
+ +
+ +
+ + + + + +
+
+ + + + \ No newline at end of file diff --git a/internal/whatsapp/whatsapp_handler.go b/internal/whatsapp/whatsapp_handler.go index e086fc3..216c20b 100644 --- a/internal/whatsapp/whatsapp_handler.go +++ b/internal/whatsapp/whatsapp_handler.go @@ -1,24 +1,156 @@ package whatsapp import ( - "log" + "html/template" + "path/filepath" "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") +type APIResponse struct { + Meta map[string]interface{} `json:"meta"` + Data interface{} `json:"data,omitempty"` +} + +func WhatsAppQRPageHandler(c *fiber.Ctx) error { + wa := config.GetWhatsAppService() + if wa == nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "WhatsApp service not initialized", + }, + }) + } + + // Jika sudah login, tampilkan halaman success + if wa.IsLoggedIn() { + templatePath := filepath.Join("internal", "whatsapp", "success_scan.html") + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "Unable to load success template: " + err.Error(), + }, + }) + } + + c.Set("Content-Type", "text/html") + return tmpl.Execute(c.Response().BodyWriter(), nil) + } + + qrDataURI, err := wa.GenerateQR() + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "Failed to generate QR code: " + err.Error(), + }, + }) + } + + if qrDataURI == "success" { + // Login berhasil, tampilkan halaman success + templatePath := filepath.Join("internal", "whatsapp", "success_scan.html") + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "Unable to load success template: " + err.Error(), + }, + }) + } + + c.Set("Content-Type", "text/html") + return tmpl.Execute(c.Response().BodyWriter(), nil) + } + + if qrDataURI == "already_connected" { + // Sudah terhubung, tampilkan halaman success + templatePath := filepath.Join("internal", "whatsapp", "success_scan.html") + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "Unable to load success template: " + err.Error(), + }, + }) + } + + c.Set("Content-Type", "text/html") + return tmpl.Execute(c.Response().BodyWriter(), nil) + } + + // Tampilkan QR code scanner + templatePath := filepath.Join("internal", "whatsapp", "scanner.html") + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "Unable to load scanner template: " + err.Error(), + }, + }) + } + + c.Set("Content-Type", "text/html") + return tmpl.Execute(c.Response().BodyWriter(), template.URL(qrDataURI)) +} + +func WhatsAppLogoutHandler(c *fiber.Ctx) error { + wa := config.GetWhatsAppService() + if wa == nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "WhatsApp service not initialized", + }, + }) + } + + err := wa.Logout() + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": err.Error(), + }, + }) + } + + return c.Status(fiber.StatusOK).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "success", + "message": "Successfully logged out and session deleted", + }, + }) +} + +func WhatsAppStatusHandler(c *fiber.Ctx) error { + wa := config.GetWhatsAppService() + if wa == nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "WhatsApp service not initialized", + }, + }) + } + + status := map[string]interface{}{ + "is_connected": wa.IsConnected(), + "is_logged_in": wa.IsLoggedIn(), + } + + return c.Status(fiber.StatusOK).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "success", + "message": "WhatsApp status retrieved successfully", + }, + Data: status, + }) } diff --git a/internal/whatsapp/whatsapp_route.go b/internal/whatsapp/whatsapp_route.go index 877bbed..76e2728 100644 --- a/internal/whatsapp/whatsapp_route.go +++ b/internal/whatsapp/whatsapp_route.go @@ -1,11 +1,11 @@ package whatsapp import ( - "rijig/middleware" - "github.com/gofiber/fiber/v2" ) func WhatsAppRouter(api fiber.Router) { - api.Post("/logout/whastapp", middleware.AuthMiddleware(), WhatsAppHandler) + api.Get("/whatsapp-status", WhatsAppStatusHandler) + api.Get("/whatsapp/pw=admin1234", WhatsAppQRPageHandler) + api.Post("/logout/whastapp", WhatsAppLogoutHandler) } diff --git a/model/user_model.go b/model/user_model.go index 7890cb1..faefec9 100644 --- a/model/user_model.go +++ b/model/user_model.go @@ -3,20 +3,20 @@ package model import "time" type User struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` - Avatar *string `json:"avatar,omitempty"` - Name string `gorm:"not null" json:"name"` - 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;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"` + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + Avatar *string `json:"avatar,omitempty"` + Name string `gorm:"not null" json:"name"` + 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;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 `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"` } diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index d06911a..5d37e8a 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -11,14 +11,15 @@ import ( "rijig/internal/userpin" "rijig/internal/whatsapp" "rijig/middleware" - // "rijig/presentation" + // "rijig/presentation" "github.com/gofiber/fiber/v2" ) func SetupRoutes(app *fiber.App) { apa := app.Group(os.Getenv("BASE_URL")) + whatsapp.WhatsAppRouter(apa) apa.Static("/uploads", "./public"+os.Getenv("BASE_URL")+"/uploads") api := app.Group(os.Getenv("BASE_URL"))