Last active
May 10, 2025 18:37
-
-
Save alexey-sh/a0c7f361f117e0c054a963fa47693869 to your computer and use it in GitHub Desktop.
Go auth service
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
"fmt" | |
"log" | |
"net/http" | |
"os" | |
"os/signal" | |
"syscall" | |
"time" | |
"github.com/gorilla/mux" | |
"github.com/redis/go-redis/v9" | |
) | |
// Config holds server configuration | |
type Config struct { | |
Port string | |
RedisAddr string | |
RedisPass string | |
ReadTimeout time.Duration | |
} | |
// Server holds dependencies | |
type Server struct { | |
config Config | |
redis *redis.Client | |
httpServer *http.Server | |
} | |
// NewServer initializes the server with dependencies | |
func NewServer(config Config) (*Server, error) { | |
// Initialize Redis client with enhanced options | |
rdb := redis.NewClient(&redis.Options{ | |
Addr: config.RedisAddr, | |
Password: config.RedisPass, | |
DB: 0, | |
PoolSize: 100, | |
MinIdleConns: 10, | |
MaxRetries: 3, | |
DialTimeout: 5 * time.Second, | |
ReadTimeout: 3 * time.Second, | |
WriteTimeout: 3 * time.Second, | |
PoolTimeout: 4 * time.Second, | |
ConnMaxLifetime: 30 * time.Minute, | |
}) | |
// Verify Redis connection | |
ctx := context.Background() | |
if err := rdb.Ping(ctx).Err(); err != nil { | |
return nil, fmt.Errorf("failed to connect to redis: %w", err) | |
} | |
// Initialize router | |
r := mux.NewRouter() | |
// Add middleware | |
r.Use(requestIDMiddleware) | |
r.Use(realIPMiddleware) | |
r.Use(recoverMiddleware) | |
r.Use(timeoutMiddleware(5 * time.Second)) | |
// Initialize server | |
s := &Server{ | |
config: config, | |
redis: rdb, | |
} | |
// Setup routes | |
r.HandleFunc("/auth", s.handleAuth).Methods("GET") | |
// Configure HTTP server | |
s.httpServer = &http.Server{ | |
Addr: ":" + config.Port, | |
Handler: r, | |
ReadTimeout: config.ReadTimeout, | |
WriteTimeout: 10 * time.Second, | |
IdleTimeout: 120 * time.Second, | |
} | |
// Start background health check | |
go s.monitorRedisHealth(ctx) | |
return s, nil | |
} | |
// requestIDMiddleware adds a unique request ID to each request | |
func requestIDMiddleware(next http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
// Use a simple UUID or timestamp-based ID for simplicity | |
reqID := fmt.Sprintf("%d", time.Now().UnixNano()) | |
r.Header.Set("X-Request-ID", reqID) | |
w.Header().Set("X-Request-ID", reqID) | |
next.ServeHTTP(w, r) | |
}) | |
} | |
// realIPMiddleware sets the real client IP based on X-Forwarded-For or RemoteAddr | |
func realIPMiddleware(next http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" { | |
r.RemoteAddr = forwarded | |
} | |
next.ServeHTTP(w, r) | |
}) | |
} | |
// recoverMiddleware catches panics and returns a 500 error | |
func recoverMiddleware(next http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
defer func() { | |
if err := recover(); err != nil { | |
log.Printf("Panic recovered: %v", err) | |
http.Error(w, "internal server error", http.StatusInternalServerError) | |
} | |
}() | |
next.ServeHTTP(w, r) | |
}) | |
} | |
// timeoutMiddleware enforces a request timeout | |
func timeoutMiddleware(timeout time.Duration) func(http.Handler) http.Handler { | |
return func(next http.Handler) http.Handler { | |
return http.TimeoutHandler(next, timeout, "request timed out") | |
} | |
} | |
// handleAuth processes GET /auth requests | |
func (s *Server) handleAuth(w http.ResponseWriter, r *http.Request) { | |
// Extract cookie | |
cookie, err := r.Cookie("sess") | |
if err != nil { | |
http.Error(w, "missing sess cookie", http.StatusUnauthorized) | |
return | |
} | |
// Extract header | |
bc := r.Header.Get("bc") | |
if bc == "" { | |
http.Error(w, "missing bc header", http.StatusUnauthorized) | |
return | |
} | |
// Create Redis key | |
key := cookie.Value + bc | |
// Query Redis with retry | |
ctx := r.Context() | |
var userID string | |
for attempt := 1; attempt <= 1; attempt++ { | |
userID, err = s.redis.HGet(ctx, key, "user_id").Result() | |
if err == nil { | |
break | |
} | |
if err == redis.Nil { | |
http.Error(w, "unauthorized", http.StatusUnauthorized) | |
return | |
} | |
log.Printf("Redis attempt %d failed: %v", attempt, err) | |
time.Sleep(time.Millisecond * 100 * time.Duration(attempt)) | |
} | |
if err != nil { | |
log.Printf("Redis error after retries: %v", err) | |
http.Error(w, "internal server error", http.StatusInternalServerError) | |
return | |
} | |
// Set response header | |
w.Header().Set("user_id", userID) | |
w.WriteHeader(http.StatusOK) | |
} | |
// monitorRedisHealth periodically checks Redis connectivity | |
func (s *Server) monitorRedisHealth(ctx context.Context) { | |
ticker := time.NewTicker(30 * time.Second) | |
defer ticker.Stop() | |
for { | |
select { | |
case <-ctx.Done(): | |
return | |
case <-ticker.C: | |
if err := s.redis.Ping(ctx).Err(); err != nil { | |
log.Printf("Redis health check failed: %v", err) | |
} | |
} | |
} | |
} | |
// Start runs the server | |
func (s *Server) Start() error { | |
log.Printf("Starting server on port %s", s.config.Port) | |
return s.httpServer.ListenAndServe() | |
} | |
// Shutdown gracefully stops the server | |
func (s *Server) Shutdown(ctx context.Context) error { | |
// Close Redis connection | |
if err := s.redis.Close(); err != nil { | |
log.Printf("Failed to close redis: %v", err) | |
} | |
// Shutdown HTTP server | |
return s.httpServer.Shutdown(ctx) | |
} | |
func main() { | |
// Load configuration | |
config := Config{ | |
Port: getEnv("PORT", "8080"), | |
RedisAddr: getEnv("REDIS_ADDR", "localhost:6379"), | |
RedisPass: getEnv("REDIS_PASS", ""), | |
ReadTimeout: 5 * time.Second, | |
} | |
// Initialize server | |
server, err := NewServer(config) | |
if err != nil { | |
log.Fatalf("Failed to initialize server: %v", err) | |
} | |
// Handle graceful shutdown | |
stop := make(chan os.Signal, 1) | |
signal.Notify(stop, os.Interrupt, syscall.SIGTERM) | |
go func() { | |
if err := server.Start(); err != nil && err != http.ErrServerClosed { | |
log.Fatalf("Server failed: %v", err) | |
} | |
}() | |
<-stop | |
log.Println("Shutting down server...") | |
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |
defer cancel() | |
if err := server.Shutdown(ctx); err != nil { | |
log.Fatalf("Server shutdown failed: %v", err) | |
} | |
log.Println("Server stopped") | |
} | |
// getEnv retrieves environment variable with fallback | |
func getEnv(key, fallback string) string { | |
if value, exists := os.LookupEnv(key); exists { | |
return value | |
} | |
return fallback | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment