new file: begushiybashkir/bbvue/src/components/AvatarUpload.vue
modified: begushiybashkir/bbvue/src/stores/auth.js modified: begushiybashkir/bbvue/src/views/Profile.vue modified: begushiybashkir/bbvue/src/views/ProfileEdit.vue modified: serv_nginx/api_bb/go.mod modified: serv_nginx/api_bb/go.sum modified: serv_nginx/api_bb/internal/handlers/auth.go new file: serv_nginx/api_bb/internal/handlers/avatar.go modified: serv_nginx/api_bb/internal/handlers/news_handler.go modified: serv_nginx/api_bb/internal/handlers/user.go modified: serv_nginx/api_bb/internal/models/user.go modified: serv_nginx/api_bb/internal/repository/user_repository.go modified: serv_nginx/api_bb/internal/routes/routes.go modified: serv_nginx/api_bb/internal/service/auth_service.go new file: serv_nginx/api_bb/internal/service/avatar_service.go modified: serv_nginx/api_bb/internal/service/news_service.go modified: serv_nginx/api_bb/internal/service/user_service.go modified: serv_nginx/api_bb/pkg/logger/interface.go new file: serv_nginx/api_bb/pkg/logger/route_logger.go add structure fix, page, path, routes, component, authStore for upload, renew and delete avatar
This commit is contained in:
@@ -21,7 +21,7 @@ import (
|
||||
type AuthHandler struct {
|
||||
authService service.AuthService
|
||||
jwtService service.JWTService
|
||||
logger logger.Interface
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
func NewAuthHandler(authService service.AuthService, jwtService service.JWTService) *AuthHandler {
|
||||
@@ -165,87 +165,86 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling login request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
h.logger.Info("handling login request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Проверяем Content-Type
|
||||
if r.Header.Get("Content-Type") != "application/json" {
|
||||
h.logger.Warn("invalid content type", zap.String("content_type", r.Header.Get("Content-Type")))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Content-Type must be application/json")
|
||||
return
|
||||
}
|
||||
// Проверяем Content-Type
|
||||
if r.Header.Get("Content-Type") != "application/json" {
|
||||
h.logger.Warn("invalid content type", zap.String("content_type", r.Header.Get("Content-Type")))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Content-Type must be application/json")
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем и логируем тело запроса
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to read request body", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Failed to read request body")
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
// Читаем и логируем тело запроса
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to read request body", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Failed to read request body")
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// Восстанавливаем тело
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
// Восстанавливаем тело
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
h.logger.Debug("request body", zap.String("body", string(bodyBytes)))
|
||||
h.logger.Debug("request body", zap.String("body", string(bodyBytes)))
|
||||
|
||||
var req LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("JSON decode failed",
|
||||
zap.Error(err),
|
||||
zap.String("raw_body", string(bodyBytes)),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
var req LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("JSON decode failed",
|
||||
zap.Error(err),
|
||||
zap.String("raw_body", string(bodyBytes)),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
req.Email = strings.TrimSpace(req.Email)
|
||||
req.Password = strings.TrimSpace(req.Password)
|
||||
req.Email = strings.TrimSpace(req.Email)
|
||||
req.Password = strings.TrimSpace(req.Password)
|
||||
|
||||
// Валидация
|
||||
if req.Email == "" || req.Password == "" {
|
||||
h.logger.Warn("validation failed",
|
||||
zap.String("email", req.Email),
|
||||
zap.Int("password_len", len(req.Password)),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Email and password are required")
|
||||
return
|
||||
}
|
||||
// Валидация
|
||||
if req.Email == "" || req.Password == "" {
|
||||
h.logger.Warn("validation failed",
|
||||
zap.String("email", req.Email),
|
||||
zap.Int("password_len", len(req.Password)),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Email and password are required")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("attempting login", zap.String("email", req.Email))
|
||||
h.logger.Info("attempting login", zap.String("email", req.Email))
|
||||
|
||||
user, token, err := h.authService.Login(req.Email, req.Password)
|
||||
if err != nil {
|
||||
h.logger.Warn("login failed", zap.String("email", req.Email), zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
user, token, err := h.authService.Login(req.Email, req.Password)
|
||||
if err != nil {
|
||||
h.logger.Warn("login failed", zap.String("email", req.Email), zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем куки
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: false,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Expires: time.Now().Add(24 * time.Hour),
|
||||
})
|
||||
// Устанавливаем куки
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: false,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Expires: time.Now().Add(24 * time.Hour),
|
||||
})
|
||||
|
||||
h.logger.Info("login successful",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", user.Email),
|
||||
)
|
||||
h.logger.Info("login successful",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", user.Email),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Login successful",
|
||||
"token": token,
|
||||
"user": toUserResponse(user),
|
||||
})
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Login successful",
|
||||
"token": token,
|
||||
"user": toUserResponse(user),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -277,4 +276,3 @@ func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
"message": "Logout successful",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// handlers/avatar.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type AvatarHandler struct {
|
||||
logger logger.LoggerInterface
|
||||
avatarService service.AvatarService
|
||||
}
|
||||
|
||||
func NewAvatarHandler(avatarService service.AvatarService) *AvatarHandler {
|
||||
return &AvatarHandler{
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "avatar"))),
|
||||
avatarService: avatarService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AvatarHandler) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
r.Post("/upload", h.UploadAvatar)
|
||||
r.Delete("/delete", h.DeleteAvatar)
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *AvatarHandler) UploadAvatar(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Парсим multipart форму
|
||||
if err := r.ParseMultipartForm(10 << 20); err != nil { // 10MB limit
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Failed to parse form: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := r.FormFile("avatar")
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Failed to get file: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Проверяем тип файла
|
||||
allowedTypes := map[string]bool{
|
||||
"image/jpeg": true,
|
||||
"image/jpg": true,
|
||||
"image/png": true,
|
||||
"image/gif": true,
|
||||
}
|
||||
if !allowedTypes[header.Header.Get("Content-Type")] {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Only JPEG, PNG and GIF images are allowed")
|
||||
return
|
||||
}
|
||||
|
||||
// Загружаем аватар
|
||||
avatarPath, err := h.avatarService.UploadAvatar(user.ID, file, header)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to upload avatar", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to upload avatar: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Avatar uploaded successfully",
|
||||
"avatar": avatarPath,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AvatarHandler) DeleteAvatar(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.avatarService.DeleteAvatar(user.ID); err != nil {
|
||||
h.logger.Error("Failed to delete avatar", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete avatar: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Avatar deleted successfully",
|
||||
})
|
||||
}
|
||||
@@ -14,265 +14,265 @@ import (
|
||||
)
|
||||
|
||||
type NewsHandler struct {
|
||||
newsService service.NewsService
|
||||
logger logger.Interface
|
||||
validator *validator.Validate
|
||||
newsService service.NewsService
|
||||
logger logger.LoggerInterface
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
func NewNewsHandler(newsService service.NewsService, log logger.Interface) *NewsHandler {
|
||||
return &NewsHandler{
|
||||
newsService: newsService,
|
||||
logger: log,
|
||||
validator: validator.New(),
|
||||
}
|
||||
func NewNewsHandler(newsService service.NewsService, log logger.LoggerInterface) *NewsHandler {
|
||||
return &NewsHandler{
|
||||
newsService: newsService,
|
||||
logger: log,
|
||||
validator: validator.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetNews возвращает список новостей с пагинацией и фильтрацией
|
||||
func (h *NewsHandler) GetNews(w http.ResponseWriter, r *http.Request) {
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
category := r.URL.Query().Get("category")
|
||||
|
||||
if limit == 0 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > 50 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
news, total, err := h.newsService.GetAllNews(limit, offset, category)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get news", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"news": news,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
category := r.URL.Query().Get("category")
|
||||
|
||||
if limit == 0 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > 50 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
news, total, err := h.newsService.GetAllNews(limit, offset, category)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get news", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"news": news,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
}
|
||||
|
||||
// GetNewsByID возвращает конкретную новость
|
||||
func (h *NewsHandler) GetNewsByID(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
news, err := h.newsService.GetNewsByID(uint(id))
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusNotFound, "News not found")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, news)
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
news, err := h.newsService.GetNewsByID(uint(id))
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusNotFound, "News not found")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, news)
|
||||
}
|
||||
|
||||
// CreateNews создает новую новость
|
||||
func (h *NewsHandler) CreateNews(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.CreateNewsRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
news, err := h.newsService.CreateNews(req, userID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create news", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusCreated, news)
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.CreateNewsRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
news, err := h.newsService.CreateNews(req, userID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create news", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusCreated, news)
|
||||
}
|
||||
|
||||
// UpdateNews обновляет новость
|
||||
func (h *NewsHandler) UpdateNews(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.UpdateNewsRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
news, err := h.newsService.UpdateNews(uint(id), req, userID)
|
||||
if err != nil {
|
||||
if err.Error() == "access denied" {
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
|
||||
return
|
||||
}
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, news)
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.UpdateNewsRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
news, err := h.newsService.UpdateNews(uint(id), req, userID)
|
||||
if err != nil {
|
||||
if err.Error() == "access denied" {
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
|
||||
return
|
||||
}
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, news)
|
||||
}
|
||||
|
||||
// DeleteNews удаляет новость
|
||||
func (h *NewsHandler) DeleteNews(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.newsService.DeleteNews(uint(id), userID)
|
||||
if err != nil {
|
||||
if err.Error() == "access denied" {
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
|
||||
return
|
||||
}
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "News deleted successfully"})
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.newsService.DeleteNews(uint(id), userID)
|
||||
if err != nil {
|
||||
if err.Error() == "access denied" {
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
|
||||
return
|
||||
}
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "News deleted successfully"})
|
||||
}
|
||||
|
||||
// CreateComment создает комментарий к новости
|
||||
func (h *NewsHandler) CreateComment(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
newsIDStr := chi.URLParam(r, "id")
|
||||
newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.CreateCommentRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
comment, err := h.newsService.CreateComment(uint(newsID), req, userID)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create comment")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusCreated, comment)
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
newsIDStr := chi.URLParam(r, "id")
|
||||
newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.CreateCommentRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
comment, err := h.newsService.CreateComment(uint(newsID), req, userID)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create comment")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusCreated, comment)
|
||||
}
|
||||
|
||||
// GetComments возвращает комментарии к новости
|
||||
func (h *NewsHandler) GetComments(w http.ResponseWriter, r *http.Request) {
|
||||
newsIDStr := chi.URLParam(r, "id")
|
||||
newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
comments, err := h.newsService.GetCommentsByNewsID(uint(newsID))
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get comments")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, comments)
|
||||
newsIDStr := chi.URLParam(r, "id")
|
||||
newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
comments, err := h.newsService.GetCommentsByNewsID(uint(newsID))
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get comments")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, comments)
|
||||
}
|
||||
|
||||
// DeleteComment удаляет комментарий
|
||||
func (h *NewsHandler) DeleteComment(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
commentIDStr := chi.URLParam(r, "commentId")
|
||||
commentID, err := strconv.ParseUint(commentIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid comment ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.newsService.DeleteComment(uint(commentID), userID)
|
||||
if err != nil {
|
||||
if err.Error() == "access denied" {
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
|
||||
return
|
||||
}
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete comment")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "Comment deleted successfully"})
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
commentIDStr := chi.URLParam(r, "commentId")
|
||||
commentID, err := strconv.ParseUint(commentIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid comment ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.newsService.DeleteComment(uint(commentID), userID)
|
||||
if err != nil {
|
||||
if err.Error() == "access denied" {
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
|
||||
return
|
||||
}
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete comment")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "Comment deleted successfully"})
|
||||
}
|
||||
|
||||
// GetUserNews возвращает новости конкретного пользователя
|
||||
func (h *NewsHandler) GetUserNews(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
|
||||
if limit == 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
news, total, err := h.newsService.GetUserNews(userID, limit, offset)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"news": news,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
userID, ok := r.Context().Value("userID").(uint)
|
||||
if !ok {
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
|
||||
if limit == 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
news, total, err := h.newsService.GetUserNews(userID, limit, offset)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user news")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"news": news,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
logger logger.Interface
|
||||
logger logger.LoggerInterface
|
||||
userService service.UserService
|
||||
}
|
||||
|
||||
|
||||
@@ -8,26 +8,29 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// models/user.go - добавить поле Avatar
|
||||
type User struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Email string `json:"email" gorm:"uniqueIndex;not null"`
|
||||
Password string `json:"-" gorm:"not null"`
|
||||
FirstName string `json:"first_name" gorm:"not null"`
|
||||
LastName string `json:"last_name" gorm:"not null"`
|
||||
Phone string `json:"phone"`
|
||||
Experience string `json:"experience"`
|
||||
Goals string `json:"goals"`
|
||||
Newsletter bool `json:"newsletter"`
|
||||
Role string `json:"role" gorm:"default:user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Email string `json:"email" gorm:"uniqueIndex;not null"`
|
||||
Password string `json:"-" gorm:"not null"`
|
||||
FirstName string `json:"first_name" gorm:"not null"`
|
||||
LastName string `json:"last_name" gorm:"not null"`
|
||||
Avatar string `json:"avatar"` // Путь к файлу аватара
|
||||
Phone string `json:"phone"`
|
||||
Experience string `json:"experience"`
|
||||
Goals string `json:"goals"`
|
||||
Newsletter bool `json:"newsletter"`
|
||||
Role string `json:"role" gorm:"default:user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
}
|
||||
|
||||
type UserUpdate struct {
|
||||
ID uint `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Avatar string `json:"avatar"` // Добавить поле аватара
|
||||
Phone string `json:"phone"`
|
||||
Experience string `json:"experience"`
|
||||
Goals string `json:"goals"`
|
||||
|
||||
@@ -8,66 +8,78 @@ import (
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
Create(user *models.User) error
|
||||
FindByID(id uint) (*models.User, error)
|
||||
FindByEmail(email string) (*models.User, error)
|
||||
Update(user *models.User) error
|
||||
Delete(id uint) error
|
||||
UpdateExcludeEmail(userUpdate *models.User) error
|
||||
Create(user *models.User) error
|
||||
FindByID(id uint) (*models.User, error)
|
||||
FindByEmail(email string) (*models.User, error)
|
||||
Update(user *models.User) error
|
||||
Delete(id uint) error
|
||||
UpdateExcludeEmail(userUpdate *models.User) error
|
||||
UpdateAvatar(userID uint, avatarPath string) error
|
||||
}
|
||||
|
||||
func (r *userRepository) UpdateAvatar(userID uint, avatarPath string) error {
|
||||
result := r.db.Model(&models.User{}).Where("id = ?", userID).Update("avatar", avatarPath)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type userRepository struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) UserRepository {
|
||||
return &userRepository{db: db}
|
||||
return &userRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *userRepository) Create(user *models.User) error {
|
||||
return r.db.Create(user).Error
|
||||
return r.db.Create(user).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByID(id uint) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.First(&user, id).Error
|
||||
return &user, err
|
||||
var user models.User
|
||||
err := r.db.First(&user, id).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByEmail(email string) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.Where("email = ?", email).First(&user).Error
|
||||
return &user, err
|
||||
var user models.User
|
||||
err := r.db.Where("email = ?", email).First(&user).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func (r *userRepository) Update(user *models.User) error {
|
||||
return r.db.Save(user).Error
|
||||
return r.db.Save(user).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.User{}, id).Error
|
||||
return r.db.Delete(&models.User{}, id).Error
|
||||
}
|
||||
|
||||
// repository/user_repository.go
|
||||
func (r *userRepository) UpdateExcludeEmail(userUpdate *models.User) error {
|
||||
// Обновляем только разрешенные поля
|
||||
result := r.db.Model(userUpdate).Where("id = ?", userUpdate.ID).Updates(map[string]interface{}{
|
||||
"first_name": userUpdate.FirstName,
|
||||
"last_name": userUpdate.LastName,
|
||||
"phone": userUpdate.Phone,
|
||||
"experience": userUpdate.Experience,
|
||||
"goals": userUpdate.Goals,
|
||||
"newsletter": userUpdate.Newsletter,
|
||||
"updated_at": userUpdate.UpdatedAt,
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
result := r.db.Model(userUpdate).Where("id = ?", userUpdate.ID).Updates(map[string]interface{}{
|
||||
"first_name": userUpdate.FirstName,
|
||||
"last_name": userUpdate.LastName,
|
||||
"avatar": userUpdate.Avatar, // Добавить обновление аватара
|
||||
"phone": userUpdate.Phone,
|
||||
"experience": userUpdate.Experience,
|
||||
"goals": userUpdate.Goals,
|
||||
"newsletter": userUpdate.Newsletter,
|
||||
"updated_at": userUpdate.UpdatedAt,
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"api_bb/internal/handlers"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger" // Добавьте импорт логгера
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
)
|
||||
|
||||
@@ -36,12 +36,14 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
||||
authService := service.NewAuthService(userRepo, jwtService, baseLogger) // Передаем логгер
|
||||
userService := service.NewUserService(userRepo, jwtService, baseLogger)
|
||||
newsService := service.NewNewsService(newsRepo, commentRepo, baseLogger)
|
||||
avatarService := service.NewAvatarService(userRepo, baseLogger)
|
||||
|
||||
// Initialize handlers
|
||||
healthHandler := handlers.NewHealthHandler()
|
||||
authHandler := handlers.NewAuthHandler(authService, jwtService)
|
||||
userHandler := handlers.NewUserHandler(&userService)
|
||||
newsHandler := handlers.NewNewsHandler(newsService, baseLogger)
|
||||
avatarHandler := handlers.NewAvatarHandler(avatarService)
|
||||
|
||||
// Health routes
|
||||
r.Mount("/api", healthHandler.Routes())
|
||||
@@ -59,6 +61,9 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
||||
r.Use(middleware.RequireAuth)
|
||||
|
||||
r.Mount("/", userHandler.Routes())
|
||||
|
||||
r.Mount("/avatar", avatarHandler.Routes())
|
||||
|
||||
// Здесь будут другие защищенные маршруты пользователя
|
||||
})
|
||||
|
||||
@@ -90,5 +95,9 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
||||
// r.Mount("/reviews", reviewHandler.Routes())
|
||||
})
|
||||
|
||||
// Логируем все зарегистрированные маршруты
|
||||
routeLogger := logger.NewRouteLogger(baseLogger)
|
||||
routeLogger.LogRoutes(r)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@ type AuthService interface {
|
||||
type authService struct {
|
||||
userRepo repository.UserRepository
|
||||
jwtService JWTService
|
||||
logger logger.Interface
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
func NewAuthService(userRepo repository.UserRepository, jwtService JWTService, log logger.Interface) AuthService {
|
||||
func NewAuthService(userRepo repository.UserRepository, jwtService JWTService, log logger.LoggerInterface) AuthService {
|
||||
// Создаем логгер с контекстом для сервиса
|
||||
serviceLogger := log.With(zap.String("service", "auth"))
|
||||
|
||||
@@ -34,7 +34,6 @@ func NewAuthService(userRepo repository.UserRepository, jwtService JWTService, l
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (s *authService) Register(user *models.User) error {
|
||||
s.logger.Info("Registering new user",
|
||||
zap.String("email", user.Email),
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// service/avatar_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/logger"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type AvatarService interface {
|
||||
UploadAvatar(userID uint, file multipart.File, header *multipart.FileHeader) (string, error)
|
||||
DeleteAvatar(userID uint) error
|
||||
GetAvatarPath(userID uint) (string, error)
|
||||
}
|
||||
|
||||
type avatarService struct {
|
||||
userRepo repository.UserRepository
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
func NewAvatarService(userRepo repository.UserRepository, log logger.LoggerInterface) AvatarService {
|
||||
return &avatarService{
|
||||
userRepo: userRepo,
|
||||
logger: log.With(zap.String("service", "avatar")),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *avatarService) UploadAvatar(userID uint, file multipart.File, header *multipart.FileHeader) (string, error) {
|
||||
// Проверяем пользователя
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
// Создаем директорию для аватаров если не существует
|
||||
uploadDir := "./uploads/avatars"
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create upload directory: %v", err)
|
||||
}
|
||||
|
||||
// Генерируем уникальное имя файла
|
||||
fileExt := filepath.Ext(header.Filename)
|
||||
fileName := fmt.Sprintf("avatar_%d_%d%s", userID, time.Now().Unix(), fileExt)
|
||||
filePath := filepath.Join(uploadDir, fileName)
|
||||
|
||||
// Создаем файл
|
||||
dst, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create file: %v", err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// Копируем содержимое
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
return "", fmt.Errorf("failed to save file: %v", err)
|
||||
}
|
||||
|
||||
// Удаляем старый аватар если существует
|
||||
if user.Avatar != "" {
|
||||
oldPath := strings.TrimPrefix(user.Avatar, "/")
|
||||
if _, err := os.Stat(oldPath); err == nil {
|
||||
os.Remove(oldPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем путь в БД
|
||||
avatarPath := "/uploads/avatars/" + fileName
|
||||
if err := s.userRepo.UpdateAvatar(userID, avatarPath); err != nil {
|
||||
// Если не удалось сохранить в БД, удаляем загруженный файл
|
||||
os.Remove(filePath)
|
||||
return "", fmt.Errorf("failed to update avatar in database: %v", err)
|
||||
}
|
||||
|
||||
return avatarPath, nil
|
||||
}
|
||||
|
||||
func (s *avatarService) DeleteAvatar(userID uint) error {
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
if user.Avatar == "" {
|
||||
return nil // Аватара нет, ничего не делаем
|
||||
}
|
||||
|
||||
// Удаляем файл
|
||||
filePath := strings.TrimPrefix(user.Avatar, "/")
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
s.logger.Warn("Failed to delete avatar file", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Очищаем поле в БД
|
||||
return s.userRepo.UpdateAvatar(userID, "")
|
||||
}
|
||||
|
||||
func (s *avatarService) GetAvatarPath(userID uint) (string, error) {
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return user.Avatar, nil
|
||||
}
|
||||
@@ -10,237 +10,236 @@ import (
|
||||
)
|
||||
|
||||
type NewsService interface {
|
||||
CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error)
|
||||
GetNewsByID(id uint) (*models.NewsResponse, error)
|
||||
GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error)
|
||||
UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error)
|
||||
DeleteNews(id uint, userID uint) error
|
||||
IncrementViews(id uint) error
|
||||
CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error)
|
||||
GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error)
|
||||
DeleteComment(commentID, userID uint) error
|
||||
GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error)
|
||||
CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error)
|
||||
GetNewsByID(id uint) (*models.NewsResponse, error)
|
||||
GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error)
|
||||
UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error)
|
||||
DeleteNews(id uint, userID uint) error
|
||||
IncrementViews(id uint) error
|
||||
CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error)
|
||||
GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error)
|
||||
DeleteComment(commentID, userID uint) error
|
||||
GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error)
|
||||
}
|
||||
|
||||
type newsService struct {
|
||||
newsRepo repository.NewsRepository
|
||||
commentRepo repository.CommentRepository
|
||||
logger logger.Interface
|
||||
newsRepo repository.NewsRepository
|
||||
commentRepo repository.CommentRepository
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
func NewNewsService(newsRepo repository.NewsRepository, commentRepo repository.CommentRepository, log logger.Interface) NewsService {
|
||||
|
||||
|
||||
func NewNewsService(newsRepo repository.NewsRepository, commentRepo repository.CommentRepository, log logger.LoggerInterface) NewsService {
|
||||
|
||||
serviceLogger := log.With(zap.String("service", "news"))
|
||||
|
||||
|
||||
return &newsService{
|
||||
newsRepo: newsRepo,
|
||||
commentRepo: commentRepo,
|
||||
logger: serviceLogger,
|
||||
}
|
||||
newsRepo: newsRepo,
|
||||
commentRepo: commentRepo,
|
||||
logger: serviceLogger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *newsService) CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error) {
|
||||
news := &models.News{
|
||||
Title: req.Title,
|
||||
Excerpt: req.Excerpt,
|
||||
Content: req.Content,
|
||||
Image: req.Image,
|
||||
Category: req.Category,
|
||||
AuthorID: authorID,
|
||||
}
|
||||
|
||||
if err := s.newsRepo.Create(news); err != nil {
|
||||
s.logger.Error("Failed to create news", zap.Error(err))
|
||||
return nil, errors.New("failed to create news")
|
||||
}
|
||||
|
||||
// Получаем созданную новость с автором
|
||||
createdNews, err := s.newsRepo.GetByID(news.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toNewsResponse(createdNews), nil
|
||||
news := &models.News{
|
||||
Title: req.Title,
|
||||
Excerpt: req.Excerpt,
|
||||
Content: req.Content,
|
||||
Image: req.Image,
|
||||
Category: req.Category,
|
||||
AuthorID: authorID,
|
||||
}
|
||||
|
||||
if err := s.newsRepo.Create(news); err != nil {
|
||||
s.logger.Error("Failed to create news", zap.Error(err))
|
||||
return nil, errors.New("failed to create news")
|
||||
}
|
||||
|
||||
// Получаем созданную новость с автором
|
||||
createdNews, err := s.newsRepo.GetByID(news.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toNewsResponse(createdNews), nil
|
||||
}
|
||||
|
||||
func (s *newsService) GetNewsByID(id uint) (*models.NewsResponse, error) {
|
||||
news, err := s.newsRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("news not found")
|
||||
}
|
||||
|
||||
// Увеличиваем счетчик просмотров
|
||||
go s.newsRepo.IncrementViews(id)
|
||||
|
||||
return s.toNewsResponse(news), nil
|
||||
news, err := s.newsRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("news not found")
|
||||
}
|
||||
|
||||
// Увеличиваем счетчик просмотров
|
||||
go s.newsRepo.IncrementViews(id)
|
||||
|
||||
return s.toNewsResponse(news), nil
|
||||
}
|
||||
|
||||
func (s *newsService) GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error) {
|
||||
news, total, err := s.newsRepo.GetAll(limit, offset, category)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
responses := make([]models.NewsResponse, len(news))
|
||||
for i, n := range news {
|
||||
responses[i] = *s.toNewsResponse(&n)
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
news, total, err := s.newsRepo.GetAll(limit, offset, category)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
responses := make([]models.NewsResponse, len(news))
|
||||
for i, n := range news {
|
||||
responses[i] = *s.toNewsResponse(&n)
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *newsService) UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error) {
|
||||
news, err := s.newsRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("news not found")
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
if news.AuthorID != userID {
|
||||
return nil, errors.New("access denied")
|
||||
}
|
||||
|
||||
// Обновляем поля
|
||||
if req.Title != "" {
|
||||
news.Title = req.Title
|
||||
}
|
||||
if req.Excerpt != "" {
|
||||
news.Excerpt = req.Excerpt
|
||||
}
|
||||
if req.Content != "" {
|
||||
news.Content = req.Content
|
||||
}
|
||||
if req.Image != "" {
|
||||
news.Image = req.Image
|
||||
}
|
||||
if req.Category != "" {
|
||||
news.Category = req.Category
|
||||
}
|
||||
|
||||
if err := s.newsRepo.Update(news); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toNewsResponse(news), nil
|
||||
news, err := s.newsRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("news not found")
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
if news.AuthorID != userID {
|
||||
return nil, errors.New("access denied")
|
||||
}
|
||||
|
||||
// Обновляем поля
|
||||
if req.Title != "" {
|
||||
news.Title = req.Title
|
||||
}
|
||||
if req.Excerpt != "" {
|
||||
news.Excerpt = req.Excerpt
|
||||
}
|
||||
if req.Content != "" {
|
||||
news.Content = req.Content
|
||||
}
|
||||
if req.Image != "" {
|
||||
news.Image = req.Image
|
||||
}
|
||||
if req.Category != "" {
|
||||
news.Category = req.Category
|
||||
}
|
||||
|
||||
if err := s.newsRepo.Update(news); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toNewsResponse(news), nil
|
||||
}
|
||||
|
||||
func (s *newsService) DeleteNews(id uint, userID uint) error {
|
||||
news, err := s.newsRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return errors.New("news not found")
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
if news.AuthorID != userID {
|
||||
return errors.New("access denied")
|
||||
}
|
||||
|
||||
return s.newsRepo.Delete(id)
|
||||
news, err := s.newsRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return errors.New("news not found")
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
if news.AuthorID != userID {
|
||||
return errors.New("access denied")
|
||||
}
|
||||
|
||||
return s.newsRepo.Delete(id)
|
||||
}
|
||||
|
||||
func (s *newsService) IncrementViews(id uint) error {
|
||||
return s.newsRepo.IncrementViews(id)
|
||||
return s.newsRepo.IncrementViews(id)
|
||||
}
|
||||
|
||||
func (s *newsService) CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error) {
|
||||
// Проверяем существование новости
|
||||
_, err := s.newsRepo.GetByID(newsID)
|
||||
if err != nil {
|
||||
return nil, errors.New("news not found")
|
||||
}
|
||||
|
||||
comment := &models.Comment{
|
||||
Content: req.Content,
|
||||
NewsID: newsID,
|
||||
AuthorID: authorID,
|
||||
}
|
||||
|
||||
if err := s.commentRepo.Create(comment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Получаем созданный комментарий с автором
|
||||
createdComment, err := s.commentRepo.GetByID(comment.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toCommentResponse(createdComment), nil
|
||||
// Проверяем существование новости
|
||||
_, err := s.newsRepo.GetByID(newsID)
|
||||
if err != nil {
|
||||
return nil, errors.New("news not found")
|
||||
}
|
||||
|
||||
comment := &models.Comment{
|
||||
Content: req.Content,
|
||||
NewsID: newsID,
|
||||
AuthorID: authorID,
|
||||
}
|
||||
|
||||
if err := s.commentRepo.Create(comment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Получаем созданный комментарий с автором
|
||||
createdComment, err := s.commentRepo.GetByID(comment.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toCommentResponse(createdComment), nil
|
||||
}
|
||||
|
||||
func (s *newsService) GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error) {
|
||||
comments, err := s.commentRepo.GetByNewsID(newsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responses := make([]models.CommentResponse, len(comments))
|
||||
for i, c := range comments {
|
||||
responses[i] = *s.toCommentResponse(&c)
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
comments, err := s.commentRepo.GetByNewsID(newsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responses := make([]models.CommentResponse, len(comments))
|
||||
for i, c := range comments {
|
||||
responses[i] = *s.toCommentResponse(&c)
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (s *newsService) DeleteComment(commentID, userID uint) error {
|
||||
comment, err := s.commentRepo.GetByID(commentID)
|
||||
if err != nil {
|
||||
return errors.New("comment not found")
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
if comment.AuthorID != userID {
|
||||
return errors.New("access denied")
|
||||
}
|
||||
|
||||
return s.commentRepo.Delete(commentID)
|
||||
comment, err := s.commentRepo.GetByID(commentID)
|
||||
if err != nil {
|
||||
return errors.New("comment not found")
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
if comment.AuthorID != userID {
|
||||
return errors.New("access denied")
|
||||
}
|
||||
|
||||
return s.commentRepo.Delete(commentID)
|
||||
}
|
||||
|
||||
func (s *newsService) GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error) {
|
||||
news, total, err := s.newsRepo.GetByAuthor(userID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
responses := make([]models.NewsResponse, len(news))
|
||||
for i, n := range news {
|
||||
responses[i] = *s.toNewsResponse(&n)
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
news, total, err := s.newsRepo.GetByAuthor(userID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
responses := make([]models.NewsResponse, len(news))
|
||||
for i, n := range news {
|
||||
responses[i] = *s.toNewsResponse(&n)
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
// Вспомогательные методы для преобразования
|
||||
func (s *newsService) toNewsResponse(news *models.News) *models.NewsResponse {
|
||||
return &models.NewsResponse{
|
||||
ID: news.ID,
|
||||
CreatedAt: news.CreatedAt,
|
||||
UpdatedAt: news.UpdatedAt,
|
||||
Title: news.Title,
|
||||
Excerpt: news.Excerpt,
|
||||
Content: news.Content,
|
||||
Image: news.Image,
|
||||
Category: news.Category,
|
||||
Views: news.Views,
|
||||
Author: models.AuthorInfo{
|
||||
ID: news.Author.ID,
|
||||
FirstName: news.Author.FirstName,
|
||||
LastName: news.Author.LastName,
|
||||
},
|
||||
Comments: len(news.Comments),
|
||||
}
|
||||
return &models.NewsResponse{
|
||||
ID: news.ID,
|
||||
CreatedAt: news.CreatedAt,
|
||||
UpdatedAt: news.UpdatedAt,
|
||||
Title: news.Title,
|
||||
Excerpt: news.Excerpt,
|
||||
Content: news.Content,
|
||||
Image: news.Image,
|
||||
Category: news.Category,
|
||||
Views: news.Views,
|
||||
Author: models.AuthorInfo{
|
||||
ID: news.Author.ID,
|
||||
FirstName: news.Author.FirstName,
|
||||
LastName: news.Author.LastName,
|
||||
},
|
||||
Comments: len(news.Comments),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *newsService) toCommentResponse(comment *models.Comment) *models.CommentResponse {
|
||||
return &models.CommentResponse{
|
||||
ID: comment.ID,
|
||||
CreatedAt: comment.CreatedAt,
|
||||
Content: comment.Content,
|
||||
Author: models.AuthorInfo{
|
||||
ID: comment.Author.ID,
|
||||
FirstName: comment.Author.FirstName,
|
||||
LastName: comment.Author.LastName,
|
||||
},
|
||||
}
|
||||
}
|
||||
return &models.CommentResponse{
|
||||
ID: comment.ID,
|
||||
CreatedAt: comment.CreatedAt,
|
||||
Content: comment.Content,
|
||||
Author: models.AuthorInfo{
|
||||
ID: comment.Author.ID,
|
||||
FirstName: comment.Author.FirstName,
|
||||
LastName: comment.Author.LastName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,16 +18,15 @@ type UserService interface {
|
||||
type userService struct {
|
||||
userRepo repository.UserRepository
|
||||
jwtService JWTService
|
||||
logger logger.Interface
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
|
||||
// UpdateProfile implements UserService.
|
||||
func (s userService) UpdateProfile(user *models.User) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func NewUserService(userRepo repository.UserRepository, jwtService JWTService, log logger.Interface) userService {
|
||||
func NewUserService(userRepo repository.UserRepository, jwtService JWTService, log logger.LoggerInterface) userService {
|
||||
// Создаем логгер с контекстом для сервиса
|
||||
serviceLogger := log.With(zap.String("service", "user"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user