/
advanceddev
/
license-server
Обзор
Документация
Войти
/
advanceddev
/
license-server
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
2
CI/CD
Аналитика
Безопасность
master
internal/handler/admin_handler.go
294 строки
8 KB
advanceddev
fix: security & reliability hardening for rate limiter, auth, and audit
10 авг 2026, 23:52
Не верифицирован
10 авг 2026, 23:52
aef6073
Код
Авторство
О чём код?
// Package handler -- package handler import ( "crypto/rand" "encoding/base64" "errors" "log/slog" "net/http" "strconv" "strings" "time" "github.com/go-chi/chi/v5" "github.com/google/uuid" "gitverse.ru/advanceddev/license-server/internal/domain" "gitverse.ru/advanceddev/license-server/internal/repository" "gitverse.ru/advanceddev/license-server/internal/validate" ) // AdminHandler - обработчик запросов к административной части. type AdminHandler struct { repo domain.LicenseRepository cache *repository.LicenseCache audit *auditWriter auditRepo domain.AuditRepository defaultLicenseDuration time.Duration } // NewAdminHandler - конструктор обработчика административной части. func NewAdminHandler( repo domain.LicenseRepository, cache *repository.LicenseCache, auditRepo domain.AuditRepository, defaultDuration time.Duration, ) *AdminHandler { return &AdminHandler{ repo: repo, cache: cache, audit: newAuditWriter(auditRepo), auditRepo: auditRepo, defaultLicenseDuration: defaultDuration, } } // Create - создание новой лицензии. func (h *AdminHandler) Create(w http.ResponseWriter, r *http.Request) { var input domain.CreateLicenseInput if err := validate.DecodeStrict(r.Body, &input); err != nil { if validate.IsDecodeError(err) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } if validate.IsValidationError(err) { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()}) return } http.Error(w, "internal error", http.StatusInternalServerError) return } existing, err := h.repo.GetByPartnerID(r.Context(), input.PartnerID) if err != nil && !errors.Is(err, domain.ErrLicenseNotFound) { http.Error(w, "internal error", http.StatusInternalServerError) return } if existing != nil { writeJSON(w, http.StatusConflict, map[string]any{ "error": "partner already has an active license", "license_id": existing.ID, "expires_at": existing.ExpiresAt, "suggestion": "Use PUT /api/v1/admin/licenses/{id} to update or extend", }) return } rawKey, err := generateLicenseKey() if err != nil { http.Error(w, "failed to generate key", http.StatusInternalServerError) return } keyHash := domain.HashLicenseKey(rawKey) expiresAt := input.ExpiresAt if expiresAt.IsZero() { expiresAt = time.Now().Add(h.defaultLicenseDuration) } license := &domain.License{ ID: uuid.New(), KeyHash: keyHash, PartnerID: input.PartnerID, BrandName: input.BrandName, Params: input.Params, ExpiresAt: expiresAt, } if err := h.repo.Create(r.Context(), license); err != nil { if errors.Is(err, domain.ErrDuplicateKey) { writeJSON(w, http.StatusConflict, map[string]string{"error": "key collision, try again"}) return } if strings.Contains(err.Error(), "partner_already_has_active_license") { writeJSON(w, http.StatusConflict, map[string]any{ "error": "partner already has an active license", "suggestion": "Use PUT /api/v1/admin/licenses/{id} to update or extend", }) return } http.Error(w, "internal error", http.StatusInternalServerError) return } writeJSON(w, http.StatusCreated, map[string]any{ "id": license.ID, "license_key": rawKey, "partner_id": license.PartnerID, "brand_name": license.BrandName, "expires_at": license.ExpiresAt, "params": license.Params, "warning": "Save this license key now. It cannot be retrieved later.", }) h.audit.Log(r, license.ID, domain.AuditActionCreated, map[string]any{ "partner_id": license.PartnerID.String(), "brand_name": license.BrandName, "expires_at": license.ExpiresAt, }) } // Update - обновление существующей лицензии. func (h *AdminHandler) Update(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") id, err := uuid.Parse(idStr) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid license id"}) return } var input domain.UpdateLicenseInput if err := validate.DecodeStrict(r.Body, &input); err != nil { if validate.IsDecodeError(err) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } if validate.IsValidationError(err) { writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()}) return } http.Error(w, "internal error", http.StatusInternalServerError) return } license, err := h.repo.Update(r.Context(), id, input) if err != nil { if errors.Is(err, domain.ErrLicenseNotFound) { writeJSON(w, http.StatusNotFound, map[string]string{"error": "license not found"}) return } if strings.Contains(err.Error(), "partner_already_has_active_license") { writeJSON(w, http.StatusConflict, map[string]any{ "error": "cannot update: partner already has another active license", "suggestion": "Delete or expire the other license first", }) return } http.Error(w, "internal error", http.StatusInternalServerError) return } if h.cache != nil { _ = h.cache.Invalidate(r.Context(), license.KeyHash) } changes := make(map[string]any) if input.BrandName != nil { changes["brand_name"] = *input.BrandName } if input.ExpiresAt != nil { changes["expires_at"] = *input.ExpiresAt } if input.Params != nil { changes["params"] = input.Params } h.audit.Log(r, license.ID, domain.AuditActionUpdated, changes) writeJSON(w, http.StatusOK, license) } // List - получение списка лицензий с пагинацией func (h *AdminHandler) List(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) if limit <= 0 || limit > 100 { limit = 20 } licenses, total, err := h.repo.List(r.Context(), limit, offset) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } writeJSON(w, http.StatusOK, map[string]any{ "data": licenses, "total": total, }) } // Delete - удаление лицензии. func (h *AdminHandler) Delete(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") id, err := uuid.Parse(idStr) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid license id"}) return } license, getErr := h.repo.GetByID(r.Context(), id) if err := h.repo.Delete(r.Context(), id); err != nil { if errors.Is(err, domain.ErrLicenseNotFound) { writeJSON(w, http.StatusNotFound, map[string]string{"error": "license not found"}) return } http.Error(w, "internal error", http.StatusInternalServerError) return } if getErr == nil && license != nil && h.cache != nil { _ = h.cache.Invalidate(r.Context(), license.KeyHash) } if getErr == nil && license != nil { h.audit.Log(r, license.ID, domain.AuditActionDeleted, map[string]any{ "partner_id": license.PartnerID.String(), "brand_name": license.BrandName, }) } writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) } // GetAuditLog - получение журнала аудита лицензии. func (h *AdminHandler) GetAuditLog(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") id, err := uuid.Parse(idStr) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid license id"}) return } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) if limit <= 0 || limit > 100 { limit = 20 } logs, total, err := h.auditRepo.ListByLicenseID(r.Context(), id, limit, offset) if err != nil { slog.Error("failed to list audit logs", "license_id", id, "error", err, ) http.Error(w, "internal error", http.StatusInternalServerError) return } writeJSON(w, http.StatusOK, map[string]any{ "data": logs, "total": total, }) } func generateLicenseKey() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } encoded := base64.RawURLEncoding.EncodeToString(b) parts := []string{"LCSR"} for i := 0; i < len(encoded) && len(parts) < 6; i += 4 { end := min(i+4, len(encoded)) parts = append(parts, encoded[i:end]) } var result strings.Builder for i, p := range parts { if i > 0 { result.WriteString("-") } result.WriteString(p) } return result.String(), nil }