package auth import ( "crypto/rand" "encoding/hex" "log/slog" "net/http" "sync" "time" ) // Session represents an authenticated user session. type Session struct { Token string ExpiresAt time.Time } // Manager handles authentication and session management. type Manager struct { mu sync.RWMutex sessions map[string]*Session username string password string expiryDur time.Duration } // New creates a new authentication manager with default credentials. func New(username, password string) *Manager { return &Manager{ sessions: make(map[string]*Session), username: username, password: password, expiryDur: 24 * time.Hour, } } // Login validates credentials and creates a session. func (m *Manager) Login(username, password string) (string, error) { if username != m.username || password != m.password { slog.Debug("Login failed - invalid credentials", "username", username) return "", ErrInvalidCredentials } token, err := generateToken() if err != nil { slog.Error("Failed to generate session token", "error", err) return "", err } m.mu.Lock() defer m.mu.Unlock() m.sessions[token] = &Session{ Token: token, ExpiresAt: time.Now().Add(m.expiryDur), } slog.Debug("Login successful, session created", "username", username, "token", token[:8]+"...") return token, nil } // Validate checks if a session token is valid. func (m *Manager) Validate(token string) bool { m.mu.RLock() defer m.mu.RUnlock() session, ok := m.sessions[token] if !ok { slog.Debug("Session validation failed - token not found", "token", token[:8]+"...") return false } if !session.ExpiresAt.After(time.Now()) { slog.Debug("Session validation failed - token expired", "token", token[:8]+"...", "expiredAt", session.ExpiresAt) return false } slog.Debug("Session validation successful", "token", token[:8]+"...") return true } // Logout invalidates a session. func (m *Manager) Logout(token string) { m.mu.Lock() defer m.mu.Unlock() delete(m.sessions, token) slog.Debug("Session logged out", "token", token[:8]+"...") } // Middleware returns a Chi middleware for authentication. func (m *Manager) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session_token") if err != nil { slog.Debug("Authentication failed - no session cookie", "path", r.URL.Path, "remoteAddr", r.RemoteAddr) http.Redirect(w, r, "/login", http.StatusSeeOther) return } if !m.Validate(cookie.Value) { slog.Debug("Authentication failed - invalid session", "path", r.URL.Path, "remoteAddr", r.RemoteAddr) http.Redirect(w, r, "/login", http.StatusSeeOther) return } slog.Debug("Authentication successful", "path", r.URL.Path, "remoteAddr", r.RemoteAddr) next.ServeHTTP(w, r) }) } // generateToken creates a random hex token. func generateToken() (string, error) { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil }