refact&fix: fixing whatsmeow and fixing air and docker setup
This commit is contained in:
parent
d7633d3c7f
commit
e06b6033b5
|
@ -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"]
|
284
Makefile
284
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
|
||||
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}}"
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}()
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
10
go.mod
10
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
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WhatsApp QR Scanner</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #25D366, #128C7E);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2rem;
|
||||
border-radius: 15px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.whatsapp-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #25D366;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-bottom: 2rem;
|
||||
opacity: 0.9;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
max-width: 300px;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
max-width: 400px;
|
||||
margin: 1rem auto;
|
||||
line-height: 1.6;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.steps {
|
||||
text-align: left;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.steps ol {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.steps li {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: rgba(46, 204, 113, 0.2);
|
||||
border: 1px solid #2ecc71;
|
||||
}
|
||||
|
||||
.status.warning {
|
||||
background: rgba(241, 196, 15, 0.2);
|
||||
border: 1px solid #f1c40f;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="logo">
|
||||
<div class="whatsapp-icon">📱</div>
|
||||
<h1>WhatsApp QR Scanner</h1>
|
||||
</div>
|
||||
|
||||
<p class="subtitle">Scan QR code untuk menghubungkan WhatsApp Anda</p>
|
||||
|
||||
<div class="qr-container">
|
||||
<img src="{{.}}" alt="WhatsApp QR Code" class="qr-code" />
|
||||
</div>
|
||||
|
||||
<div class="instructions">
|
||||
<div class="steps">
|
||||
<h3>Cara menggunakan:</h3>
|
||||
<ol>
|
||||
<li>Buka WhatsApp di ponsel Anda</li>
|
||||
<li>Tap Menu atau Settings dan pilih WhatsApp Web</li>
|
||||
<li>Arahkan ponsel Anda ke QR code ini untuk memindainya</li>
|
||||
<li>Tunggu hingga terhubung</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="status warning">
|
||||
<span class="loading"></span>
|
||||
Menunggu pemindaian QR code...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 30000);
|
||||
|
||||
setInterval(function() {
|
||||
fetch('/api/whatsapp-status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.meta.status === 'success' && data.data.is_connected) {
|
||||
document.querySelector('.status').innerHTML = '✅ WhatsApp berhasil terhubung!';
|
||||
document.querySelector('.status').className = 'status success';
|
||||
|
||||
setTimeout(function() {
|
||||
alert('WhatsApp berhasil terhubung! Anda dapat menutup halaman ini.');
|
||||
}, 2000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Status check error:', error);
|
||||
});
|
||||
}, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,411 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WhatsApp - Berhasil Terhubung</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #25D366, #128C7E);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2rem;
|
||||
border-radius: 15px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.whatsapp-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #25D366;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 4rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
margin: 1.5rem 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.indicator.connected {
|
||||
background: #2ecc71;
|
||||
box-shadow: 0 0 10px rgba(46, 204, 113, 0.5);
|
||||
}
|
||||
|
||||
.indicator.disconnected {
|
||||
background: #e74c3c;
|
||||
box-shadow: 0 0 10px rgba(231, 76, 60, 0.5);
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2980b9;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c0392b;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin: 1rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.alert.success {
|
||||
background: rgba(46, 204, 113, 0.2);
|
||||
border: 1px solid #2ecc71;
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
.alert.error {
|
||||
background: rgba(231, 76, 60, 0.2);
|
||||
border: 1px solid #e74c3c;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="logo">
|
||||
<div class="whatsapp-icon">📱</div>
|
||||
<h1>WhatsApp Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div class="success-icon">✅</div>
|
||||
|
||||
<div class="status-message" id="statusMessage">
|
||||
WhatsApp berhasil terhubung dan siap digunakan!
|
||||
</div>
|
||||
|
||||
<div class="status-info">
|
||||
<div class="status-item">
|
||||
<span class="status-label">Status Koneksi:</span>
|
||||
<div class="status-value">
|
||||
<span class="indicator" id="connectionIndicator"></span>
|
||||
<span id="connectionStatus">Checking...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Status Login:</span>
|
||||
<div class="status-value">
|
||||
<span class="indicator" id="loginIndicator"></span>
|
||||
<span id="loginStatus">Checking...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alertContainer"></div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" onclick="refreshStatus()">
|
||||
<span class="loading hidden" id="refreshLoading"></span>
|
||||
<span id="refreshText">🔄 Refresh Status</span>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary" onclick="window.location.href='/api/whatsapp/pw=admin1234'">
|
||||
📱 Lihat QR Code
|
||||
</button>
|
||||
|
||||
<button class="btn btn-danger" onclick="logoutWhatsApp()">
|
||||
<span class="loading hidden" id="logoutLoading"></span>
|
||||
<span id="logoutText">🚪 Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let statusCheckInterval;
|
||||
|
||||
function showAlert(message, type = 'success') {
|
||||
const alertContainer = document.getElementById('alertContainer');
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert ${type}`;
|
||||
alert.textContent = message;
|
||||
alertContainer.innerHTML = '';
|
||||
alertContainer.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
alert.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function updateStatusUI(data) {
|
||||
const connectionIndicator = document.getElementById('connectionIndicator');
|
||||
const connectionStatus = document.getElementById('connectionStatus');
|
||||
const loginIndicator = document.getElementById('loginIndicator');
|
||||
const loginStatus = document.getElementById('loginStatus');
|
||||
const statusMessage = document.getElementById('statusMessage');
|
||||
|
||||
// Update connection status
|
||||
if (data.is_connected) {
|
||||
connectionIndicator.className = 'indicator connected';
|
||||
connectionStatus.textContent = 'Terhubung';
|
||||
} else {
|
||||
connectionIndicator.className = 'indicator disconnected';
|
||||
connectionStatus.textContent = 'Terputus';
|
||||
}
|
||||
|
||||
// Update login status
|
||||
if (data.is_logged_in) {
|
||||
loginIndicator.className = 'indicator connected';
|
||||
loginStatus.textContent = 'Login';
|
||||
} else {
|
||||
loginIndicator.className = 'indicator disconnected';
|
||||
loginStatus.textContent = 'Belum Login';
|
||||
}
|
||||
|
||||
// Update main message
|
||||
if (data.is_connected && data.is_logged_in) {
|
||||
statusMessage.textContent = 'WhatsApp berhasil terhubung dan siap digunakan!';
|
||||
} else if (data.is_logged_in && !data.is_connected) {
|
||||
statusMessage.textContent = 'WhatsApp sudah login tetapi tidak terhubung. Silakan refresh atau restart koneksi.';
|
||||
} else {
|
||||
statusMessage.textContent = 'WhatsApp belum terhubung dengan baik. Silakan scan QR code kembali.';
|
||||
}
|
||||
}
|
||||
|
||||
function checkStatus() {
|
||||
fetch('/api/whatsapp-status')
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.meta.status === 'success') {
|
||||
updateStatusUI(result.data);
|
||||
} else {
|
||||
console.error('Status check failed:', result.meta.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Status check error:', error);
|
||||
const connectionIndicator = document.getElementById('connectionIndicator');
|
||||
const connectionStatus = document.getElementById('connectionStatus');
|
||||
const loginIndicator = document.getElementById('loginIndicator');
|
||||
const loginStatus = document.getElementById('loginStatus');
|
||||
|
||||
connectionIndicator.className = 'indicator disconnected';
|
||||
connectionStatus.textContent = 'Error';
|
||||
loginIndicator.className = 'indicator disconnected';
|
||||
loginStatus.textContent = 'Error';
|
||||
});
|
||||
}
|
||||
|
||||
function refreshStatus() {
|
||||
const refreshLoading = document.getElementById('refreshLoading');
|
||||
const refreshText = document.getElementById('refreshText');
|
||||
|
||||
refreshLoading.classList.remove('hidden');
|
||||
refreshText.textContent = 'Refreshing...';
|
||||
|
||||
checkStatus();
|
||||
|
||||
setTimeout(() => {
|
||||
refreshLoading.classList.add('hidden');
|
||||
refreshText.textContent = '🔄 Refresh Status';
|
||||
showAlert('Status berhasil diperbarui!');
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function logoutWhatsApp() {
|
||||
if (!confirm('Apakah Anda yakin ingin logout dari WhatsApp?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logoutLoading = document.getElementById('logoutLoading');
|
||||
const logoutText = document.getElementById('logoutText');
|
||||
|
||||
logoutLoading.classList.remove('hidden');
|
||||
logoutText.textContent = 'Logging out...';
|
||||
|
||||
fetch('/api/logout/whastapp', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.meta.status === 'success') {
|
||||
showAlert('Berhasil logout dari WhatsApp!');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/api/whatsapp/pw=admin1234';
|
||||
}, 2000);
|
||||
} else {
|
||||
showAlert(result.meta.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Logout error:', error);
|
||||
showAlert('Terjadi kesalahan saat logout', 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
logoutLoading.classList.add('hidden');
|
||||
logoutText.textContent = '🚪 Logout';
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkStatus();
|
||||
statusCheckInterval = setInterval(checkStatus, 10000); // Check every 10 seconds
|
||||
});
|
||||
|
||||
// Cleanup interval when page is unloaded
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (statusCheckInterval) {
|
||||
clearInterval(statusCheckInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
|
|
Loading…
Reference in New Issue