first commit
This commit is contained in:
No files matched your search
@@ -0,0 +1,132 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
models "api-service/internal/models/auth"
|
||||
services "api-service/internal/services/auth"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AuthHandler handles authentication endpoints
|
||||
type AuthHandler struct {
|
||||
authService *services.AuthService
|
||||
}
|
||||
|
||||
// NewAuthHandler creates a new authentication handler
|
||||
func NewAuthHandler(authService *services.AuthService) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
// Login godoc
|
||||
// @Summary Login user and get JWT token
|
||||
// @Description Authenticate user with username and password to receive JWT token
|
||||
// @Tags Authentication
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param login body models.LoginRequest true "Login credentials"
|
||||
// @Success 200 {object} models.TokenResponse
|
||||
// @Failure 400 {object} map[string]string "Bad request"
|
||||
// @Failure 401 {object} map[string]string "Unauthorized"
|
||||
// @Router /api/v1/auth/login [post]
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var loginReq models.LoginRequest
|
||||
|
||||
// Bind JSON request
|
||||
if err := c.ShouldBindJSON(&loginReq); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticate user
|
||||
tokenResponse, err := h.authService.Login(loginReq.Username, loginReq.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tokenResponse)
|
||||
}
|
||||
|
||||
// RefreshToken godoc
|
||||
// @Summary Refresh JWT token
|
||||
// @Description Refresh the JWT token using a valid refresh token
|
||||
// @Tags Authentication
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param refresh body map[string]string true "Refresh token"
|
||||
// @Success 200 {object} models.TokenResponse
|
||||
// @Failure 400 {object} map[string]string "Bad request"
|
||||
// @Failure 401 {object} map[string]string "Unauthorized"
|
||||
// @Router /api/v1/auth/refresh [post]
|
||||
func (h *AuthHandler) RefreshToken(c *gin.Context) {
|
||||
// For now, this is a placeholder for refresh token functionality
|
||||
// In a real implementation, you would handle refresh tokens here
|
||||
c.JSON(http.StatusNotImplemented, gin.H{"error": "refresh token not implemented"})
|
||||
}
|
||||
|
||||
// Register godoc
|
||||
// @Summary Register new user
|
||||
// @Description Register a new user account
|
||||
// @Tags Authentication
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param register body map[string]string true "Registration data"
|
||||
// @Success 201 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string "Bad request"
|
||||
// @Router /api/v1/auth/register [post]
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var registerReq struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
Role string `json:"role" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(®isterReq); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.authService.RegisterUser(
|
||||
registerReq.Username,
|
||||
registerReq.Email,
|
||||
registerReq.Password,
|
||||
registerReq.Role,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"message": "user registered successfully"})
|
||||
}
|
||||
|
||||
// Me godoc
|
||||
// @Summary Get current user info
|
||||
// @Description Get information about the currently authenticated user
|
||||
// @Tags Authentication
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 401 {object} map[string]string "Unauthorized"
|
||||
// @Router /api/v1/auth/me [get]
|
||||
func (h *AuthHandler) Me(c *gin.Context) {
|
||||
// Get user info from context (set by middleware)
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
// In a real implementation, you would fetch user details from database
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": userID,
|
||||
"username": c.GetString("username"),
|
||||
"email": c.GetString("email"),
|
||||
"role": c.GetString("role"),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
models "api-service/internal/models/auth"
|
||||
services "api-service/internal/services/auth"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// TokenHandler handles token generation endpoints
|
||||
type TokenHandler struct {
|
||||
authService *services.AuthService
|
||||
}
|
||||
|
||||
// NewTokenHandler creates a new token handler
|
||||
func NewTokenHandler(authService *services.AuthService) *TokenHandler {
|
||||
return &TokenHandler{
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateToken godoc
|
||||
// @Summary Generate JWT token
|
||||
// @Description Generate a JWT token for a user
|
||||
// @Tags Token
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param token body models.LoginRequest true "User credentials"
|
||||
// @Success 200 {object} models.TokenResponse
|
||||
// @Failure 400 {object} map[string]string "Bad request"
|
||||
// @Failure 401 {object} map[string]string "Unauthorized"
|
||||
// @Router /api/v1/token/generate [post]
|
||||
func (h *TokenHandler) GenerateToken(c *gin.Context) {
|
||||
var loginReq models.LoginRequest
|
||||
|
||||
// Bind JSON request
|
||||
if err := c.ShouldBindJSON(&loginReq); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate token
|
||||
tokenResponse, err := h.authService.Login(loginReq.Username, loginReq.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tokenResponse)
|
||||
}
|
||||
|
||||
// GenerateTokenDirect godoc
|
||||
// @Summary Generate token directly
|
||||
// @Description Generate a JWT token directly without password verification (for testing)
|
||||
// @Tags Token
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user body map[string]string true "User info"
|
||||
// @Success 200 {object} models.TokenResponse
|
||||
// @Failure 400 {object} map[string]string "Bad request"
|
||||
// @Router /api/v1/token/generate-direct [post]
|
||||
func (h *TokenHandler) GenerateTokenDirect(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Email string `json:"email" binding:"required"`
|
||||
Role string `json:"role" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Create a temporary user for token generation
|
||||
user := &models.User{
|
||||
ID: "temp-" + req.Username,
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
Role: req.Role,
|
||||
}
|
||||
|
||||
// Generate token directly
|
||||
token, err := h.authService.GenerateTokenForUser(user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.TokenResponse{
|
||||
AccessToken: token,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"api-service/internal/database"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HealthCheckHandler handles health check requests
|
||||
type HealthCheckHandler struct {
|
||||
dbService database.Service
|
||||
}
|
||||
|
||||
// NewHealthCheckHandler creates a new HealthCheckHandler
|
||||
func NewHealthCheckHandler(dbService database.Service) *HealthCheckHandler {
|
||||
return &HealthCheckHandler{dbService: dbService}
|
||||
}
|
||||
|
||||
// CheckHealth checks the health of the application
|
||||
func (h *HealthCheckHandler) CheckHealth(c *gin.Context) {
|
||||
healthStatus := h.dbService.Health() // Call the health check function from the database service
|
||||
c.JSON(http.StatusOK, healthStatus)
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
@@ -0,0 +1,111 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WebSocketBroadcaster defines the interface for broadcasting messages
|
||||
type WebSocketBroadcaster interface {
|
||||
BroadcastMessage(messageType string, data interface{})
|
||||
}
|
||||
|
||||
// Broadcaster handles server-initiated broadcasts to WebSocket clients
|
||||
type Broadcaster struct {
|
||||
handler WebSocketBroadcaster
|
||||
tickers []*time.Ticker
|
||||
quit chan struct{}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewBroadcaster creates a new Broadcaster instance
|
||||
func NewBroadcaster(handler WebSocketBroadcaster) *Broadcaster {
|
||||
return &Broadcaster{
|
||||
handler: handler,
|
||||
tickers: make([]*time.Ticker, 0),
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// StartHeartbeat starts sending periodic heartbeat messages to all clients
|
||||
func (b *Broadcaster) StartHeartbeat(interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
b.tickers = append(b.tickers, ticker)
|
||||
go func() {
|
||||
defer func() {
|
||||
// Remove ticker from slice when done
|
||||
for i, t := range b.tickers {
|
||||
if t == ticker {
|
||||
b.tickers = append(b.tickers[:i], b.tickers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
b.handler.BroadcastMessage("heartbeat", map[string]interface{}{
|
||||
"message": "Server heartbeat",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
case <-b.quit:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop stops the broadcaster
|
||||
func (b *Broadcaster) Stop() {
|
||||
close(b.quit)
|
||||
for _, ticker := range b.tickers {
|
||||
if ticker != nil {
|
||||
ticker.Stop()
|
||||
}
|
||||
}
|
||||
b.tickers = nil
|
||||
}
|
||||
|
||||
// BroadcastNotification sends a notification message to all clients
|
||||
func (b *Broadcaster) BroadcastNotification(title, message, level string) {
|
||||
b.handler.BroadcastMessage("notification", map[string]interface{}{
|
||||
"title": title,
|
||||
"message": message,
|
||||
"level": level,
|
||||
"time": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// SimulateDataStream simulates streaming data to clients (useful for demos)
|
||||
func (b *Broadcaster) SimulateDataStream() {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
b.tickers = append(b.tickers, ticker)
|
||||
go func() {
|
||||
defer func() {
|
||||
// Remove ticker from slice when done
|
||||
for i, t := range b.tickers {
|
||||
if t == ticker {
|
||||
b.tickers = append(b.tickers[:i], b.tickers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
counter := 0
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
counter++
|
||||
b.handler.BroadcastMessage("data_stream", map[string]interface{}{
|
||||
"id": counter,
|
||||
"value": counter * 10,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"type": "simulated_data",
|
||||
})
|
||||
case <-b.quit:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MockWebSocketHandler is a mock implementation for testing
|
||||
type MockWebSocketHandler struct {
|
||||
mu sync.Mutex
|
||||
messages []map[string]interface{}
|
||||
broadcasts []string
|
||||
}
|
||||
|
||||
func (m *MockWebSocketHandler) BroadcastMessage(messageType string, data interface{}) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.broadcasts = append(m.broadcasts, messageType)
|
||||
m.messages = append(m.messages, map[string]interface{}{
|
||||
"type": messageType,
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *MockWebSocketHandler) GetMessages() []map[string]interface{} {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
result := make([]map[string]interface{}, len(m.messages))
|
||||
copy(result, m.messages)
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *MockWebSocketHandler) GetBroadcasts() []string {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
result := make([]string, len(m.broadcasts))
|
||||
copy(result, m.broadcasts)
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *MockWebSocketHandler) Clear() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.messages = make([]map[string]interface{}, 0)
|
||||
m.broadcasts = make([]string, 0)
|
||||
}
|
||||
|
||||
func NewMockWebSocketHandler() *MockWebSocketHandler {
|
||||
return &MockWebSocketHandler{
|
||||
messages: make([]map[string]interface{}, 0),
|
||||
broadcasts: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroadcaster_StartHeartbeat(t *testing.T) {
|
||||
mockHandler := NewMockWebSocketHandler()
|
||||
broadcaster := NewBroadcaster(mockHandler)
|
||||
|
||||
// Start heartbeat with short interval for testing
|
||||
broadcaster.StartHeartbeat(100 * time.Millisecond)
|
||||
|
||||
// Wait for a few heartbeats
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
|
||||
// Stop the broadcaster
|
||||
broadcaster.Stop()
|
||||
|
||||
// Check if heartbeats were sent
|
||||
messages := mockHandler.GetMessages()
|
||||
if len(messages) == 0 {
|
||||
t.Error("Expected heartbeat messages, but got none")
|
||||
}
|
||||
|
||||
// Check that all messages are heartbeat type
|
||||
broadcasts := mockHandler.GetBroadcasts()
|
||||
for _, msgType := range broadcasts {
|
||||
if msgType != "heartbeat" {
|
||||
t.Errorf("Expected heartbeat message type, got %s", msgType)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Received %d heartbeat messages", len(messages))
|
||||
}
|
||||
|
||||
func TestBroadcaster_BroadcastNotification(t *testing.T) {
|
||||
mockHandler := NewMockWebSocketHandler()
|
||||
broadcaster := NewBroadcaster(mockHandler)
|
||||
|
||||
// Send a notification
|
||||
broadcaster.BroadcastNotification("Test Title", "Test Message", "info")
|
||||
|
||||
// Check if notification was sent
|
||||
messages := mockHandler.GetMessages()
|
||||
if len(messages) != 1 {
|
||||
t.Errorf("Expected 1 message, got %d", len(messages))
|
||||
return
|
||||
}
|
||||
|
||||
msg := messages[0]
|
||||
if msg["type"] != "notification" {
|
||||
t.Errorf("Expected message type 'notification', got %s", msg["type"])
|
||||
}
|
||||
|
||||
data := msg["data"].(map[string]interface{})
|
||||
if data["title"] != "Test Title" {
|
||||
t.Errorf("Expected title 'Test Title', got %s", data["title"])
|
||||
}
|
||||
if data["message"] != "Test Message" {
|
||||
t.Errorf("Expected message 'Test Message', got %s", data["message"])
|
||||
}
|
||||
if data["level"] != "info" {
|
||||
t.Errorf("Expected level 'info', got %s", data["level"])
|
||||
}
|
||||
|
||||
t.Logf("Notification sent successfully: %+v", data)
|
||||
}
|
||||
|
||||
func TestBroadcaster_SimulateDataStream(t *testing.T) {
|
||||
mockHandler := NewMockWebSocketHandler()
|
||||
broadcaster := NewBroadcaster(mockHandler)
|
||||
|
||||
// Start data stream with short interval for testing
|
||||
broadcaster.SimulateDataStream()
|
||||
|
||||
// Wait for a few data points
|
||||
time.Sleep(550 * time.Millisecond)
|
||||
|
||||
// Stop the broadcaster
|
||||
broadcaster.Stop()
|
||||
|
||||
// Check if data stream messages were sent
|
||||
messages := mockHandler.GetMessages()
|
||||
if len(messages) == 0 {
|
||||
t.Error("Expected data stream messages, but got none")
|
||||
}
|
||||
|
||||
// Check that all messages are data_stream type
|
||||
broadcasts := mockHandler.GetBroadcasts()
|
||||
for _, msgType := range broadcasts {
|
||||
if msgType != "data_stream" {
|
||||
t.Errorf("Expected data_stream message type, got %s", msgType)
|
||||
}
|
||||
}
|
||||
|
||||
// Check data structure
|
||||
for i, msg := range messages {
|
||||
data := msg["data"].(map[string]interface{})
|
||||
if data["type"] != "simulated_data" {
|
||||
t.Errorf("Expected data type 'simulated_data', got %s", data["type"])
|
||||
}
|
||||
if id, ok := data["id"].(int); ok {
|
||||
if id != i+1 {
|
||||
t.Errorf("Expected id %d, got %d", i+1, id)
|
||||
}
|
||||
}
|
||||
if value, ok := data["value"].(int); ok {
|
||||
expectedValue := (i + 1) * 10
|
||||
if value != expectedValue {
|
||||
t.Errorf("Expected value %d, got %d", expectedValue, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Received %d data stream messages", len(messages))
|
||||
}
|
||||
|
||||
func TestBroadcaster_Stop(t *testing.T) {
|
||||
mockHandler := NewMockWebSocketHandler()
|
||||
broadcaster := NewBroadcaster(mockHandler)
|
||||
|
||||
// Start heartbeat
|
||||
broadcaster.StartHeartbeat(50 * time.Millisecond)
|
||||
|
||||
// Wait a bit
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Stop the broadcaster
|
||||
broadcaster.Stop()
|
||||
|
||||
// Clear previous messages
|
||||
mockHandler.Clear()
|
||||
|
||||
// Wait a bit more to ensure no new messages are sent
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Check that no new messages were sent after stopping
|
||||
messages := mockHandler.GetMessages()
|
||||
if len(messages) > 0 {
|
||||
t.Errorf("Expected no messages after stopping, but got %d", len(messages))
|
||||
}
|
||||
|
||||
// Clear quit channel to allow reuse in tests
|
||||
broadcaster.quit = make(chan struct{})
|
||||
|
||||
t.Log("Broadcaster stopped successfully")
|
||||
}
|
||||
|
||||
func TestBroadcaster_MultipleOperations(t *testing.T) {
|
||||
mockHandler := NewMockWebSocketHandler()
|
||||
broadcaster := NewBroadcaster(mockHandler)
|
||||
|
||||
// Start heartbeat
|
||||
broadcaster.StartHeartbeat(100 * time.Millisecond)
|
||||
|
||||
// Send notification
|
||||
broadcaster.BroadcastNotification("Test", "Message", "warning")
|
||||
|
||||
// Start data stream
|
||||
broadcaster.SimulateDataStream()
|
||||
|
||||
// Wait for some activity
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
|
||||
// Stop everything
|
||||
broadcaster.Stop()
|
||||
|
||||
// Check results
|
||||
messages := mockHandler.GetMessages()
|
||||
if len(messages) == 0 {
|
||||
t.Error("Expected messages from multiple operations, but got none")
|
||||
}
|
||||
|
||||
broadcasts := mockHandler.GetBroadcasts()
|
||||
hasHeartbeat := false
|
||||
hasNotification := false
|
||||
hasDataStream := false
|
||||
|
||||
for _, msgType := range broadcasts {
|
||||
switch msgType {
|
||||
case "heartbeat":
|
||||
hasHeartbeat = true
|
||||
case "notification":
|
||||
hasNotification = true
|
||||
case "data_stream":
|
||||
hasDataStream = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasHeartbeat {
|
||||
t.Error("Expected heartbeat messages")
|
||||
}
|
||||
if !hasNotification {
|
||||
t.Error("Expected notification message")
|
||||
}
|
||||
if !hasDataStream {
|
||||
t.Error("Expected data stream messages")
|
||||
}
|
||||
|
||||
t.Logf("Multiple operations test passed: %d total messages", len(messages))
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
Reference in New Issue
Block a user