// Package api реализует HTTP API pulse-lets-go. package api import ( "context" "encoding/json" "log" "net/http" "strings" "time" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" "github.com/pulse-lets-go/internal/models" ) // JWT claims type claims struct { UserID string `json:"user_id"` Username string `json:"username"` Role string `json:"role"` jwt.RegisteredClaims } // contextKey — тип для ключей context. type contextKey string const ( userContextKey contextKey = "user" refreshTokenKey contextKey = "refresh_token" ) // UserInfo — информация о пользователе, извлекаемая из JWT. type UserInfo struct { UserID string Username string Role string } const ( jwtExpiry = 15 * time.Minute refreshExpiry = 24 * time.Hour ) // generateJWT создаёт access+refresh токены. func (a *API) generateJWT(user *models.User) (*models.LoginResponse, error) { now := time.Now() // Access token accessClaims := claims{ UserID: user.ID, Username: user.Username, Role: string(user.Role), RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(jwtExpiry)), IssuedAt: jwt.NewNumericDate(now), }, } accessToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString([]byte(a.jwtSecret)) if err != nil { return nil, err } // Refresh token refreshClaims := claims{ UserID: user.ID, Username: user.Username, Role: string(user.Role), RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(refreshExpiry)), IssuedAt: jwt.NewNumericDate(now), }, } refreshToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString([]byte(a.jwtSecret)) if err != nil { return nil, err } return &models.LoginResponse{ AccessToken: accessToken, RefreshToken: refreshToken, ExpiresIn: int64(jwtExpiry.Seconds()), }, nil } // authMiddleware — проверяет JWT в заголовке Authorization. func (a *API) authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tokenStr := extractBearerToken(r) if tokenStr == "" { writeError(w, http.StatusUnauthorized, "требуется авторизация") return } c := &claims{} token, err := jwt.ParseWithClaims(tokenStr, c, func(token *jwt.Token) (interface{}, error) { return []byte(a.jwtSecret), nil }) if err != nil || !token.Valid { writeError(w, http.StatusUnauthorized, "недействительный токен") return } user := &UserInfo{ UserID: c.UserID, Username: c.Username, Role: c.Role, } ctx := context.WithValue(r.Context(), userContextKey, user) next.ServeHTTP(w, r.WithContext(ctx)) }) } // adminOnly — middleware проверки роли admin. func (a *API) adminOnly(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := GetUserFromContext(r.Context()) if user == nil || user.Role != string(models.RoleAdmin) { writeError(w, http.StatusForbidden, "требуется роль admin") return } next.ServeHTTP(w, r) }) } // apiKeyMiddleware — проверяет API-key для monitoring endpoint. func (a *API) apiKeyMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { key := r.Header.Get("X-API-Key") if key == "" || key != a.monitoringAPIKey { writeError(w, http.StatusUnauthorized, "недействительный API-ключ") return } next.ServeHTTP(w, r) }) } // handleLogin обрабатывает POST /api/auth/login. func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) { var req models.LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "некорректный JSON") return } if req.Username == "" || req.Password == "" { writeError(w, http.StatusBadRequest, "имя и пароль обязательны") return } users, err := a.configManager.ReadUsers() if err != nil { log.Printf("[auth] ошибка чтения users.json: %v", err) writeError(w, http.StatusInternalServerError, "ошибка сервера") return } var found *models.User for i := range users { if users[i].Username == req.Username { found = &users[i] break } } if found == nil { writeError(w, http.StatusUnauthorized, "неверное имя или пароль") return } // Если пароль не задан (плейсхолдер), разрешаем вход с паролем "admin" if found.PasswordHash == "" { if req.Password != "admin" { writeError(w, http.StatusUnauthorized, "неверное имя или пароль") return } // Хешируем пароль и сохраняем hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) found.PasswordHash = string(hash) a.configManager.SaveUsers(users) } else { if err := bcrypt.CompareHashAndPassword([]byte(found.PasswordHash), []byte(req.Password)); err != nil { writeError(w, http.StatusUnauthorized, "неверное имя или пароль") return } } resp, err := a.generateJWT(found) if err != nil { log.Printf("[auth] ошибка генерации JWT: %v", err) writeError(w, http.StatusInternalServerError, "ошибка сервера") return } writeJSON(w, http.StatusOK, resp) } // handleRefresh выдаёт новый access токен по refresh токену. func (a *API) handleRefresh(w http.ResponseWriter, r *http.Request) { tokenStr := extractBearerToken(r) if tokenStr == "" { // пробуем получить из query param (для WebSocket) tokenStr = r.URL.Query().Get("token") } if tokenStr == "" { writeError(w, http.StatusUnauthorized, "требуется токен") return } c := &claims{} token, err := jwt.ParseWithClaims(tokenStr, c, func(token *jwt.Token) (interface{}, error) { return []byte(a.jwtSecret), nil }) if err != nil || !token.Valid { writeError(w, http.StatusUnauthorized, "недействительный refresh токен") return } user := &models.User{ ID: c.UserID, Username: c.Username, Role: models.UserRole(c.Role), } resp, err := a.generateJWT(user) if err != nil { writeError(w, http.StatusInternalServerError, "ошибка сервера") return } writeJSON(w, http.StatusOK, resp) } // --- Хелперы --- // extractBearerToken извлекает JWT из заголовка Authorization: Bearer . func extractBearerToken(r *http.Request) string { auth := r.Header.Get("Authorization") if auth == "" { return "" } parts := strings.SplitN(auth, " ", 2) if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") { return "" } return parts[1] } // GetUserFromContext извлекает UserInfo из контекста запроса. func GetUserFromContext(ctx context.Context) *UserInfo { user, ok := ctx.Value(userContextKey).(*UserInfo) if !ok { return nil } return user } // writeJSON отправляет JSON-ответ. func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) } // writeError отправляет JSON-ошибку. func writeError(w http.ResponseWriter, status int, msg string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]string{"error": msg}) }