modified: begushiybashkir/bbvue/src/router/index.js

modified:   begushiybashkir/bbvue/src/views/Login.vue
	new file:   begushiybashkir/bbvue/src/views/Logout.vue
	modified:   begushiybashkir/bbvue/src/views/News.vue
	modified:   begushiybashkir/bbvue/src/views/Profile.vue
	modified:   serv_nginx/api_bb/cmd/main.go
	modified:   serv_nginx/api_bb/go.mod
	modified:   serv_nginx/api_bb/go.sum
	new file:   serv_nginx/api_bb/internal/app/app.go
	new file:   serv_nginx/api_bb/internal/database/database.go
	new file:   serv_nginx/api_bb/internal/database/migrate.go
	new file:   serv_nginx/api_bb/internal/handlers/news_handler.go
	new file:   serv_nginx/api_bb/internal/models/news.go
	new file:   serv_nginx/api_bb/internal/repository/comment_repository.go
	new file:   serv_nginx/api_bb/internal/repository/news_repository.go
	modified:   serv_nginx/api_bb/internal/routes/routes.go
	new file:   serv_nginx/api_bb/internal/service/news_service.go
	modified:   serv_nginx/api_bb/pkg/utils/utils.go
save router paths to login  logout profile from upsunction commit
This commit is contained in:
2025-10-12 21:38:50 +05:00
parent 12f805f9e1
commit 6bb475acb2
18 changed files with 1424 additions and 309 deletions
+17 -104
View File
@@ -2,29 +2,22 @@
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"api_bb/internal/app"
"api_bb/internal/config"
"api_bb/pkg/logger"
"go.uber.org/zap"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"api_bb/internal/config"
"api_bb/internal/models"
"api_bb/internal/routes"
"api_bb/pkg/logger"
)
func main() {
// Загрузка конфигурации
cfg := config.Load()
// Инициализация логгера
if err := logger.Init(
os.Getenv("LOG_LEVEL"),
@@ -35,58 +28,13 @@ func main() {
}
defer logger.Sync()
zapLogger := logger.Get()
// Логируем начало работы
logger.LogApplicationStart(os.Getenv("REST_API_VERSION"), os.Getenv("ENVIRONMENT"), "")
// Логирование попытки подключения к БД
zapLogger.Info("attempting to connect to database",
zap.String("host", extractHostFromDSN(cfg.DatabaseURL)), // функция для извлечения хоста из DSN
zap.String("database", extractDBNameFromDSN(cfg.DatabaseURL)), // функция для извлечения имени БД
)
// Подключение к базе данных
db, err := gorm.Open(postgres.Open(cfg.DatabaseURL), &gorm.Config{})
if err != nil {
zapLogger.Fatal("failed to connect to database",
zap.Error(err),
zap.String("database_url", maskPassword(cfg.DatabaseURL)), // маскируем пароль в логах
)
}
// Логирование успешного подключения к БД
zapLogger.Info("successfully connected to database",
zap.String("host", extractHostFromDSN(cfg.DatabaseURL)),
zap.String("database", extractDBNameFromDSN(cfg.DatabaseURL)),
)
// Проверка соединения с БД
sqlDB, err := db.DB()
if err != nil {
zapLogger.Fatal("failed to get database instance", zap.Error(err))
}
if err := sqlDB.Ping(); err != nil {
zapLogger.Fatal("database ping failed", zap.Error(err))
}
zapLogger.Info("database ping successful")
// Автомиграция
zapLogger.Info("starting database migration")
if err := db.AutoMigrate(&models.User{}); err != nil {
zapLogger.Fatal("database migration failed", zap.Error(err))
}
zapLogger.Info("database migration completed successfully")
// Настройка роутера
router := routes.SetupRouter(db, cfg)
// Настройка HTTP сервера
server := &http.Server{
Addr: ":" + cfg.Port,
Handler: router,
// Создание и инициализация приложения
application := app.NewApp(cfg)
if err := application.Initialize(); err != nil {
logger.Get().Fatal("failed to initialize application", zap.Error(err))
}
// Канал для graceful shutdown
@@ -96,56 +44,21 @@ func main() {
// Запуск сервера в горутине
go func() {
zapLogger.Info("starting HTTP server", zap.String("port", cfg.Port))
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
zapLogger.Fatal("failed to start server", zap.Error(err))
if err := application.Start(); err != nil {
logger.Get().Fatal("failed to start server", zap.Error(err))
}
done <- true
}()
// Ожидание сигнала shutdown
<-quit
zapLogger.Info("shutdown signal received")
logger.Get().Info("shutdown signal received")
// Логирование закрытия соединения с БД
zapLogger.Info("closing database connection")
if err := sqlDB.Close(); err != nil {
zapLogger.Error("failed to close database connection", zap.Error(err))
} else {
zapLogger.Info("database connection closed successfully")
}
// Graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
server.SetKeepAlivesEnabled(false)
if err := server.Shutdown(ctx); err != nil {
zapLogger.Fatal("could not gracefully shutdown the server", zap.Error(err))
// Graceful shutdown приложения
if err := application.Shutdown(); err != nil {
logger.Get().Fatal("could not gracefully shutdown the application", zap.Error(err))
}
logger.LogApplicationShutdown("graceful shutdown")
close(done)
}
// Вспомогательные функции для работы с DSN
// extractHostFromDSN извлекает хост из DSN строки
func extractHostFromDSN(dsn string) string {
// Простая реализация - в продакшене лучше использовать парсер DSN
// Для postgres DSN формата: "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"
// Можно использовать более сложный парсер или регулярные выражения
return dsn // Заглушка - нужно реализовать парсинг DSN
}
// extractDBNameFromDSN извлекает имя базы данных из DSN строки
func extractDBNameFromDSN(dsn string) string {
// Аналогично extractHostFromDSN - нужно реализовать парсинг
return dsn // Заглушка - нужно реализовать парсинг DSN
}
// maskPassword маскирует пароль в DSN строке для безопасного логирования
func maskPassword(dsn string) string {
// Простая реализация - заменяет пароль на ***
// В продакшене нужно использовать более надежный метод
return dsn // Заглушка - нужно реализовать маскирование пароля
<-done
}
+9 -1
View File
@@ -11,9 +11,17 @@ require (
gorm.io/gorm v1.31.0
)
require go.uber.org/multierr v1.10.0 // indirect
require (
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/sys v0.37.0 // indirect
)
require (
github.com/go-playground/validator/v10 v10.28.0
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
+12
View File
@@ -1,10 +1,18 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@@ -21,6 +29,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -36,6 +46,8 @@ golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+108
View File
@@ -0,0 +1,108 @@
package app
import (
"context"
"net/http"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"api_bb/internal/config"
"api_bb/internal/database"
"api_bb/internal/routes"
"api_bb/pkg/logger"
)
type App struct {
cfg *config.Config
db *database.Database
server *http.Server
}
func NewApp(cfg *config.Config) *App {
return &App{
cfg: cfg,
}
}
// Initialize инициализирует приложение (БД, миграции, роутинг)
func (a *App) Initialize() error {
zapLogger := logger.Get()
// Инициализация базы данных
dbConfig := &database.Config{
URL: a.cfg.DatabaseURL,
}
a.db = database.NewDatabase(dbConfig)
// Подключение к БД
if err := a.db.Connect(); err != nil {
return err
}
// Проверка соединения
if err := a.db.Ping(); err != nil {
return err
}
// Выполнение миграций
if err := a.db.Migrate(); err != nil {
return err
}
// Настройка роутера
router := routes.SetupRouter(a.db.DB, a.cfg)
// Настройка HTTP сервера
a.server = &http.Server{
Addr: ":" + a.cfg.Port,
Handler: router,
}
zapLogger.Info("application initialized successfully")
return nil
}
// Start запускает HTTP сервер
func (a *App) Start() error {
zapLogger := logger.Get()
zapLogger.Info("starting HTTP server", zap.String("port", a.cfg.Port))
if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}
return nil
}
// Shutdown gracefully останавливает приложение
func (a *App) Shutdown() error {
zapLogger := logger.Get()
zapLogger.Info("shutdown signal received")
// Graceful shutdown сервера
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
a.server.SetKeepAlivesEnabled(false)
if err := a.server.Shutdown(ctx); err != nil {
zapLogger.Error("could not gracefully shutdown the server", zap.Error(err))
return err
}
// Закрытие соединения с БД
if err := a.db.Close(); err != nil {
return err
}
zapLogger.Info("application shutdown completed")
return nil
}
// GetDB возвращает экземпляр базы данных
func (a *App) GetDB() *gorm.DB {
return a.db.DB
}
@@ -0,0 +1,139 @@
package database
import (
"fmt"
"strings"
"go.uber.org/zap"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"api_bb/pkg/logger"
)
type Database struct {
DB *gorm.DB
cfg *Config
}
type Config struct {
URL string
}
func NewDatabase(cfg *Config) *Database {
return &Database{
cfg: cfg,
}
}
// Connect устанавливает соединение с базой данных
func (d *Database) Connect() error {
zapLogger := logger.Get()
// Логирование попытки подключения к БД
zapLogger.Info("attempting to connect to database",
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
)
db, err := gorm.Open(postgres.Open(d.cfg.URL), &gorm.Config{})
if err != nil {
zapLogger.Error("failed to connect to database",
zap.Error(err),
zap.String("database_url", MaskPassword(d.cfg.URL)),
)
return fmt.Errorf("failed to connect to database: %w", err)
}
d.DB = db
// Логирование успешного подключения к БД
zapLogger.Info("successfully connected to database",
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
)
return nil
}
// Ping проверяет соединение с базой данных
func (d *Database) Ping() error {
zapLogger := logger.Get()
sqlDB, err := d.DB.DB()
if err != nil {
zapLogger.Error("failed to get database instance", zap.Error(err))
return fmt.Errorf("failed to get database instance: %w", err)
}
if err := sqlDB.Ping(); err != nil {
zapLogger.Error("database ping failed", zap.Error(err))
return fmt.Errorf("database ping failed: %w", err)
}
zapLogger.Info("database ping successful")
return nil
}
// Close закрывает соединение с базой данных
func (d *Database) Close() error {
zapLogger := logger.Get()
if d.DB == nil {
return nil
}
sqlDB, err := d.DB.DB()
if err != nil {
zapLogger.Error("failed to get database instance for closing", zap.Error(err))
return fmt.Errorf("failed to get database instance: %w", err)
}
zapLogger.Info("closing database connection")
if err := sqlDB.Close(); err != nil {
zapLogger.Error("failed to close database connection", zap.Error(err))
return fmt.Errorf("failed to close database connection: %w", err)
}
zapLogger.Info("database connection closed successfully")
return nil
}
// Вспомогательные функции для работы с DSN
// ExtractHostFromDSN извлекает хост из DSN строки
func ExtractHostFromDSN(dsn string) string {
// Простая реализация для PostgreSQL DSN
parts := strings.Split(dsn, " ")
for _, part := range parts {
if strings.HasPrefix(part, "host=") {
return strings.TrimPrefix(part, "host=")
}
}
return "unknown"
}
// ExtractDBNameFromDSN извлекает имя базы данных из DSN строки
func ExtractDBNameFromDSN(dsn string) string {
// Простая реализация для PostgreSQL DSN
parts := strings.Split(dsn, " ")
for _, part := range parts {
if strings.HasPrefix(part, "dbname=") {
return strings.TrimPrefix(part, "dbname=")
}
}
return "unknown"
}
// MaskPassword маскирует пароль в DSN строке для безопасного логирования
func MaskPassword(dsn string) string {
// Простая реализация - заменяет пароль на ***
parts := strings.Split(dsn, " ")
for i, part := range parts {
if strings.HasPrefix(part, "password=") {
parts[i] = "password=***"
break
}
}
return strings.Join(parts, " ")
}
@@ -0,0 +1,78 @@
package database
import (
"go.uber.org/zap"
"api_bb/internal/models"
"api_bb/pkg/logger"
)
// Migrate выполняет автоматические миграции для всех моделей
func (d *Database) Migrate() error {
zapLogger := logger.Get()
zapLogger.Info("starting database migration")
// Список всех моделей для миграции
models := []interface{}{
&models.User{},
&models.News{},
&models.Comment{},
// Добавьте другие модели здесь
}
for _, model := range models {
modelName := getModelName(model)
zapLogger.Debug("migrating model", zap.String("model", modelName))
if err := d.DB.AutoMigrate(model); err != nil {
zapLogger.Error("failed to migrate model",
zap.String("model", modelName),
zap.Error(err),
)
return err
}
}
zapLogger.Info("database migration completed successfully")
return nil
}
// MigrateModels выполняет миграции для конкретных моделей
func (d *Database) MigrateModels(models ...interface{}) error {
zapLogger := logger.Get()
zapLogger.Info("starting migration for specific models",
zap.Int("model_count", len(models)),
)
for _, model := range models {
modelName := getModelName(model)
zapLogger.Debug("migrating model", zap.String("model", modelName))
if err := d.DB.AutoMigrate(model); err != nil {
zapLogger.Error("failed to migrate model",
zap.String("model", modelName),
zap.Error(err),
)
return err
}
}
zapLogger.Info("models migration completed successfully")
return nil
}
// getModelName возвращает имя модели для логирования
func getModelName(model interface{}) string {
switch model.(type) {
case *models.User:
return "User"
case *models.News:
return "News"
case *models.Comment:
return "Comment"
default:
return "Unknown"
}
}
@@ -0,0 +1,278 @@
package handlers
import (
"api_bb/internal/models"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/utils"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/go-playground/validator/v10"
"go.uber.org/zap"
)
type NewsHandler struct {
newsService service.NewsService
logger logger.Interface
validator *validator.Validate
}
func NewNewsHandler(newsService service.NewsService, log logger.Interface) *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,
})
}
// 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)
}
// 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)
}
// 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)
}
// 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"})
}
// 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)
}
// 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)
}
// 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"})
}
// 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,
})
}
+100
View File
@@ -0,0 +1,100 @@
package models
import (
"time"
"gorm.io/gorm"
)
type NewsCategory string
const (
NewsCategoryEvents NewsCategory = "events"
NewsCategoryTraining NewsCategory = "training"
NewsCategoryAchievements NewsCategory = "achievements"
NewsCategoryCommunity NewsCategory = "community"
)
type News struct {
ID uint `json:"id" gorm:"primarykey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
Title string `json:"title" gorm:"size:255;not null"`
Excerpt string `json:"excerpt" gorm:"size:500;not null"`
Content string `json:"content" gorm:"type:text;not null"`
Image string `json:"image" gorm:"size:255"`
Category NewsCategory `json:"category" gorm:"type:varchar(20);not null"`
Views int `json:"views" gorm:"default:0"`
// Связи
AuthorID uint `json:"author_id" gorm:"not null"`
Author User `json:"author" gorm:"foreignKey:AuthorID"`
Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:NewsID"`
}
type Comment struct {
ID uint `json:"id" gorm:"primarykey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Content string `json:"content" gorm:"type:text;not null"`
// Связи
NewsID uint `json:"news_id" gorm:"not null"`
AuthorID uint `json:"author_id" gorm:"not null"`
Author User `json:"author" gorm:"foreignKey:AuthorID"`
}
// DTO для создания новости
type CreateNewsRequest struct {
Title string `json:"title" validate:"required,min=5,max=255"`
Excerpt string `json:"excerpt" validate:"required,min=10,max=500"`
Content string `json:"content" validate:"required,min=50"`
Image string `json:"image"`
Category NewsCategory `json:"category" validate:"required,oneof=events training achievements community"`
}
// DTO для обновления новости
type UpdateNewsRequest struct {
Title string `json:"title" validate:"omitempty,min=5,max=255"`
Excerpt string `json:"excerpt" validate:"omitempty,min=10,max=500"`
Content string `json:"content" validate:"omitempty,min=50"`
Image string `json:"image"`
Category NewsCategory `json:"category" validate:"omitempty,oneof=events training achievements community"`
}
// DTO для ответа с новостью
type NewsResponse struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Title string `json:"title"`
Excerpt string `json:"excerpt"`
Content string `json:"content"`
Image string `json:"image"`
Category NewsCategory `json:"category"`
Views int `json:"views"`
Author AuthorInfo `json:"author"`
Comments int `json:"comments_count"`
}
type AuthorInfo struct {
ID uint `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email,omitempty"`
}
// DTO для комментария
type CreateCommentRequest struct {
Content string `json:"content" validate:"required,min=1,max=1000"`
}
type CommentResponse struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
Content string `json:"content"`
Author AuthorInfo `json:"author"`
}
@@ -0,0 +1,42 @@
package repository
import (
"api_bb/internal/models"
"gorm.io/gorm"
)
type CommentRepository interface {
Create(comment *models.Comment) error
GetByNewsID(newsID uint) ([]models.Comment, error)
Delete(id uint) error
GetByID(id uint) (*models.Comment, error)
}
type commentRepository struct {
db *gorm.DB
}
func NewCommentRepository(db *gorm.DB) CommentRepository {
return &commentRepository{db: db}
}
func (r *commentRepository) Create(comment *models.Comment) error {
return r.db.Create(comment).Error
}
func (r *commentRepository) GetByNewsID(newsID uint) ([]models.Comment, error) {
var comments []models.Comment
err := r.db.Preload("Author").Where("news_id = ?", newsID).
Order("created_at ASC").Find(&comments).Error
return comments, err
}
func (r *commentRepository) Delete(id uint) error {
return r.db.Delete(&models.Comment{}, id).Error
}
func (r *commentRepository) GetByID(id uint) (*models.Comment, error) {
var comment models.Comment
err := r.db.Preload("Author").Where("id = ?", id).First(&comment).Error
return &comment, err
}
@@ -0,0 +1,88 @@
package repository
import (
"api_bb/internal/models"
"gorm.io/gorm"
)
type NewsRepository interface {
Create(news *models.News) error
GetByID(id uint) (*models.News, error)
GetAll(limit, offset int, category string) ([]models.News, int64, error)
Update(news *models.News) error
Delete(id uint) error
IncrementViews(id uint) error
GetByAuthor(authorID uint, limit, offset int) ([]models.News, int64, error)
}
type newsRepository struct {
db *gorm.DB
}
func NewNewsRepository(db *gorm.DB) NewsRepository {
return &newsRepository{db: db}
}
func (r *newsRepository) Create(news *models.News) error {
return r.db.Create(news).Error
}
func (r *newsRepository) GetByID(id uint) (*models.News, error) {
var news models.News
err := r.db.Preload("Author").Preload("Comments.Author").
Where("id = ?", id).First(&news).Error
return &news, err
}
func (r *newsRepository) GetAll(limit, offset int, category string) ([]models.News, int64, error) {
var news []models.News
var total int64
query := r.db.Preload("Author")
if category != "" && category != "all" {
query = query.Where("category = ?", category)
}
// Получаем общее количество
if err := query.Model(&models.News{}).Count(&total).Error; err != nil {
return nil, 0, err
}
// Получаем данные с пагинацией
err := query.Order("created_at DESC").
Limit(limit).Offset(offset).
Find(&news).Error
return news, total, err
}
func (r *newsRepository) Update(news *models.News) error {
return r.db.Save(news).Error
}
func (r *newsRepository) Delete(id uint) error {
return r.db.Delete(&models.News{}, id).Error
}
func (r *newsRepository) IncrementViews(id uint) error {
return r.db.Model(&models.News{}).Where("id = ?", id).
Update("views", gorm.Expr("views + ?", 1)).Error
}
func (r *newsRepository) GetByAuthor(authorID uint, limit, offset int) ([]models.News, int64, error) {
var news []models.News
var total int64
query := r.db.Preload("Author").Where("author_id = ?", authorID)
if err := query.Model(&models.News{}).Count(&total).Error; err != nil {
return nil, 0, err
}
err := query.Order("created_at DESC").
Limit(limit).Offset(offset).
Find(&news).Error
return news, total, err
}
@@ -25,6 +25,8 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
// Initialize repositories
userRepo := repository.NewUserRepository(db)
newsRepo := repository.NewNewsRepository(db)
commentRepo := repository.NewCommentRepository(db)
// Initialize logger
baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер
@@ -33,11 +35,13 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
jwtService := service.NewJWTService(config.JWTSecret)
authService := service.NewAuthService(userRepo, jwtService, baseLogger) // Передаем логгер
userService := service.NewUserService(userRepo, jwtService, baseLogger)
newsService := service.NewNewsService(newsRepo, commentRepo, baseLogger)
// Initialize handlers
healthHandler := handlers.NewHealthHandler()
authHandler := handlers.NewAuthHandler(authService, jwtService)
userHandler := handlers.NewUserHandler(&userService)
newsHandler := handlers.NewNewsHandler(newsService, baseLogger)
// Health routes
r.Mount("/api", healthHandler.Routes())
@@ -58,6 +62,29 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
// Здесь будут другие защищенные маршруты пользователя
})
r.Route("/news", func(r chi.Router) {
// Публичные маршруты
r.Get("/", newsHandler.GetNews)
r.Get("/{id}", newsHandler.GetNewsByID)
r.Get("/{id}/comments", newsHandler.GetComments)
r.Get("/check", healthHandler.Check)
// Защищенные маршруты
r.Group(func(r chi.Router) {
r.Use(middleware.AuthMiddleware(jwtService, userRepo))
r.Use(middleware.RequireAuth)
r.Post("/", newsHandler.CreateNews)
r.Put("/{id}", newsHandler.UpdateNews)
r.Delete("/{id}", newsHandler.DeleteNews)
r.Post("/{id}/comments", newsHandler.CreateComment)
r.Delete("/comments/{commentId}", newsHandler.DeleteComment)
r.Get("/my/news", newsHandler.GetUserNews)
r.Get("/check", healthHandler.Check)
})
})
// Здесь будут добавлены другие маршруты:
// r.Mount("/events", eventHandler.Routes())
// r.Mount("/reviews", reviewHandler.Routes())
@@ -0,0 +1,246 @@
package service
import (
"api_bb/internal/models"
"api_bb/internal/repository"
"api_bb/pkg/logger"
"errors"
"go.uber.org/zap"
)
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)
}
type newsService struct {
newsRepo repository.NewsRepository
commentRepo repository.CommentRepository
logger logger.Interface
}
func NewNewsService(newsRepo repository.NewsRepository, commentRepo repository.CommentRepository, log logger.Interface) NewsService {
serviceLogger := log.With(zap.String("service", "news"))
return &newsService{
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
}
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
}
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
}
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
}
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)
}
func (s *newsService) IncrementViews(id uint) error {
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
}
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
}
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)
}
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
}
// Вспомогательные методы для преобразования
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),
}
}
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,
},
}
}
+60 -1
View File
@@ -2,7 +2,11 @@ package utils
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
)
func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) {
@@ -13,4 +17,59 @@ func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) {
func RespondWithError(w http.ResponseWriter, statusCode int, message string) {
RespondWithJSON(w, statusCode, map[string]string{"error": message})
}
}
// DecodeJSONBody декодирует JSON тело запроса
func DecodeJSONBody(w http.ResponseWriter, r *http.Request, dst interface{}) error {
if r.Header.Get("Content-Type") != "application/json" {
return errors.New("Content-Type header is not application/json")
}
r.Body = http.MaxBytesReader(w, r.Body, 1048576) // 1MB limit
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
err := dec.Decode(dst)
if err != nil {
var syntaxError *json.SyntaxError
var unmarshalTypeError *json.UnmarshalTypeError
switch {
case errors.As(err, &syntaxError):
return fmt.Errorf("request body contains badly-formed JSON (at position %d)", syntaxError.Offset)
case errors.Is(err, io.ErrUnexpectedEOF):
return errors.New("request body contains badly-formed JSON")
case errors.As(err, &unmarshalTypeError):
return fmt.Errorf("request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset)
case strings.HasPrefix(err.Error(), "json: unknown field "):
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
return fmt.Errorf("request body contains unknown field %s", fieldName)
case errors.Is(err, io.EOF):
return errors.New("request body must not be empty")
case err.Error() == "http: request body too large":
return errors.New("request body must not be larger than 1MB")
default:
return err
}
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
return errors.New("request body must only contain a single JSON object")
}
return nil
}
// GetUserIDFromContext извлекает userID из контекста
func GetUserIDFromContext(r *http.Request) (uint, bool) {
userID, ok := r.Context().Value("userID").(uint)
return userID, ok
}