asd
This commit is contained in:
@@ -177,6 +177,155 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/api/users:
|
||||
get:
|
||||
tags:
|
||||
- Social Rating
|
||||
summary: List users with current social rating
|
||||
operationId: listUsersWithRatings
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 200
|
||||
default: 50
|
||||
responses:
|
||||
'200':
|
||||
description: Users with current rating values
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UsersResponse'
|
||||
'401':
|
||||
description: Missing or invalid token
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/api/users/{userId}:
|
||||
get:
|
||||
tags:
|
||||
- Social Rating
|
||||
summary: Get user with current social rating
|
||||
operationId: getUserWithRating
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: userId
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: uint64
|
||||
responses:
|
||||
'200':
|
||||
description: User with current rating value
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserRatingResponse'
|
||||
'400':
|
||||
description: Invalid user id
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'401':
|
||||
description: Missing or invalid token
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'404':
|
||||
description: User not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/api/users/{userId}/social-rating/history:
|
||||
get:
|
||||
tags:
|
||||
- Social Rating
|
||||
summary: Get social rating history for user
|
||||
operationId: getUserRatingHistory
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: userId
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: uint64
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 200
|
||||
default: 50
|
||||
responses:
|
||||
'200':
|
||||
description: Rating history for user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HistoryResponse'
|
||||
'400':
|
||||
description: Invalid user id
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'401':
|
||||
description: Missing or invalid token
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'404':
|
||||
description: User not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/api/social-rating/operations:
|
||||
get:
|
||||
tags:
|
||||
- Social Rating
|
||||
summary: Get recent social rating operations
|
||||
operationId: getRecentSocialRatingOperations
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 200
|
||||
default: 50
|
||||
responses:
|
||||
'200':
|
||||
description: Recent rating operations across all users
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HistoryResponse'
|
||||
'401':
|
||||
description: Missing or invalid token
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/api/social-rating/decrease:
|
||||
post:
|
||||
tags:
|
||||
@@ -321,6 +470,34 @@ components:
|
||||
required:
|
||||
- user
|
||||
|
||||
UsersResponse:
|
||||
type: object
|
||||
properties:
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/UserWithRating'
|
||||
required:
|
||||
- users
|
||||
|
||||
UserRatingResponse:
|
||||
type: object
|
||||
properties:
|
||||
user:
|
||||
$ref: '#/components/schemas/UserWithRating'
|
||||
required:
|
||||
- user
|
||||
|
||||
HistoryResponse:
|
||||
type: object
|
||||
properties:
|
||||
operations:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SocialRatingOperation'
|
||||
required:
|
||||
- operations
|
||||
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
@@ -350,6 +527,42 @@ components:
|
||||
- createdAt
|
||||
- updatedAt
|
||||
|
||||
UserWithRating:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: uint64
|
||||
example: 2
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
example: user@example.com
|
||||
isAdmin:
|
||||
type: boolean
|
||||
example: false
|
||||
score:
|
||||
type: integer
|
||||
example: -4
|
||||
lastOperationId:
|
||||
type: integer
|
||||
format: uint64
|
||||
nullable: true
|
||||
example: 15
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
required:
|
||||
- id
|
||||
- email
|
||||
- isAdmin
|
||||
- score
|
||||
- createdAt
|
||||
- updatedAt
|
||||
|
||||
UserSocialRating:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
@@ -16,10 +17,21 @@ import (
|
||||
func NewRouter(db *gorm.DB, cfg config.Config) *gin.Engine {
|
||||
router := gin.Default()
|
||||
router.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"http://localhost:5173", "http://127.0.0.1:5173", "http://localhost:8081", "http://127.0.0.1:8081", "https://social-rating.nekiiinkognito.ru/"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowOriginFunc: func(origin string) bool {
|
||||
switch {
|
||||
case strings.HasPrefix(origin, "http://localhost:"):
|
||||
return true
|
||||
case strings.HasPrefix(origin, "http://127.0.0.1:"):
|
||||
return true
|
||||
case origin == "https://social-rating.nekiiinkognito.ru":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 12 * time.Hour,
|
||||
}))
|
||||
@@ -38,6 +50,10 @@ func NewRouter(db *gorm.DB, cfg config.Config) *gin.Engine {
|
||||
protected := api.Group("/")
|
||||
protected.Use(auth.Middleware(cfg.JWTSecret))
|
||||
protected.GET("/auth/me", authHandler.Me)
|
||||
protected.GET("/users", socialRatingHandler.ListUsers)
|
||||
protected.GET("/users/:userId", socialRatingHandler.GetUser)
|
||||
protected.GET("/users/:userId/social-rating/history", socialRatingHandler.GetUserHistory)
|
||||
protected.GET("/social-rating/operations", socialRatingHandler.GetRecentOperations)
|
||||
protected.POST("/social-rating/increase", socialRatingHandler.Increase)
|
||||
protected.POST("/social-rating/decrease", socialRatingHandler.Decrease)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package socialrating
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -26,10 +27,79 @@ type ChangeResponse struct {
|
||||
CurrentRating models.UserSocialRating `json:"currentRating"`
|
||||
}
|
||||
|
||||
type UsersResponse struct {
|
||||
Users []UserWithRating `json:"users"`
|
||||
}
|
||||
|
||||
type UserRatingResponse struct {
|
||||
User UserWithRating `json:"user"`
|
||||
}
|
||||
|
||||
type HistoryResponse struct {
|
||||
Operations []models.SocialRatingOperation `json:"operations"`
|
||||
}
|
||||
|
||||
func NewHandler(service Service) Handler {
|
||||
return Handler{Service: service}
|
||||
}
|
||||
|
||||
func (h Handler) ListUsers(ctx *gin.Context) {
|
||||
limit := parsePositiveLimit(ctx.Query("limit"), 50)
|
||||
|
||||
users, err := h.Service.ListUsersWithRatings(limit)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load users"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, UsersResponse{Users: users})
|
||||
}
|
||||
|
||||
func (h Handler) GetUser(ctx *gin.Context) {
|
||||
userID, err := parseUintParam(ctx.Param("userId"))
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.Service.GetUserRating(userID)
|
||||
if err != nil {
|
||||
handleApplyChangeError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, UserRatingResponse{User: user})
|
||||
}
|
||||
|
||||
func (h Handler) GetUserHistory(ctx *gin.Context) {
|
||||
userID, err := parseUintParam(ctx.Param("userId"))
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
limit := parsePositiveLimit(ctx.Query("limit"), 50)
|
||||
operations, err := h.Service.GetUserHistory(userID, limit)
|
||||
if err != nil {
|
||||
handleApplyChangeError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, HistoryResponse{Operations: operations})
|
||||
}
|
||||
|
||||
func (h Handler) GetRecentOperations(ctx *gin.Context) {
|
||||
limit := parsePositiveLimit(ctx.Query("limit"), 50)
|
||||
|
||||
operations, err := h.Service.GetRecentOperations(limit)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load operations"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, HistoryResponse{Operations: operations})
|
||||
}
|
||||
|
||||
func (h Handler) Increase(ctx *gin.Context) {
|
||||
h.applySignedChange(ctx, "increase", 1)
|
||||
}
|
||||
@@ -97,3 +167,12 @@ func handleApplyChangeError(ctx *gin.Context, err error) {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
}
|
||||
}
|
||||
|
||||
func parseUintParam(raw string) (uint, error) {
|
||||
value, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil || value == 0 {
|
||||
return 0, errors.New("invalid user id")
|
||||
}
|
||||
|
||||
return uint(value), nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package socialrating
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -22,6 +24,16 @@ type ChangeInput struct {
|
||||
Source string
|
||||
}
|
||||
|
||||
type UserWithRating struct {
|
||||
ID uint `json:"id"`
|
||||
Email string `json:"email"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
Score int `json:"score"`
|
||||
LastOperationID *uint `json:"lastOperationId,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func NewService(db *gorm.DB) Service {
|
||||
return Service{DB: db}
|
||||
}
|
||||
@@ -95,3 +107,131 @@ func ensureUserExists(tx *gorm.DB, userID uint) error {
|
||||
var user models.User
|
||||
return tx.First(&user, "id = ?", userID).Error
|
||||
}
|
||||
|
||||
func (s Service) ListUsersWithRatings(limit int) ([]UserWithRating, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
ID uint
|
||||
Email string
|
||||
IsAdmin bool
|
||||
Score int
|
||||
LastOperationID *uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
err := s.DB.
|
||||
Table("users").
|
||||
Select(
|
||||
"users.id, users.email, users.is_admin, COALESCE(user_social_ratings.score, 0) AS score, user_social_ratings.last_operation_id, users.created_at, users.updated_at",
|
||||
).
|
||||
Joins("LEFT JOIN user_social_ratings ON user_social_ratings.user_id = users.id").
|
||||
Order("score DESC, users.id ASC").
|
||||
Limit(limit).
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]UserWithRating, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
result = append(result, UserWithRating{
|
||||
ID: row.ID,
|
||||
Email: row.Email,
|
||||
IsAdmin: row.IsAdmin,
|
||||
Score: row.Score,
|
||||
LastOperationID: row.LastOperationID,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s Service) GetUserRating(userID uint) (UserWithRating, error) {
|
||||
var row struct {
|
||||
ID uint
|
||||
Email string
|
||||
IsAdmin bool
|
||||
Score int
|
||||
LastOperationID *uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
err := s.DB.
|
||||
Table("users").
|
||||
Select(
|
||||
"users.id, users.email, users.is_admin, COALESCE(user_social_ratings.score, 0) AS score, user_social_ratings.last_operation_id, users.created_at, users.updated_at",
|
||||
).
|
||||
Joins("LEFT JOIN user_social_ratings ON user_social_ratings.user_id = users.id").
|
||||
Where("users.id = ?", userID).
|
||||
Take(&row).Error
|
||||
if err != nil {
|
||||
return UserWithRating{}, err
|
||||
}
|
||||
|
||||
return UserWithRating{
|
||||
ID: row.ID,
|
||||
Email: row.Email,
|
||||
IsAdmin: row.IsAdmin,
|
||||
Score: row.Score,
|
||||
LastOperationID: row.LastOperationID,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s Service) GetUserHistory(userID uint, limit int) ([]models.SocialRatingOperation, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
if err := ensureUserExists(s.DB, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var operations []models.SocialRatingOperation
|
||||
err := s.DB.
|
||||
Where("target_user_id = ?", userID).
|
||||
Order("created_at DESC, id DESC").
|
||||
Limit(limit).
|
||||
Find(&operations).Error
|
||||
|
||||
return operations, err
|
||||
}
|
||||
|
||||
func (s Service) GetRecentOperations(limit int) ([]models.SocialRatingOperation, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
var operations []models.SocialRatingOperation
|
||||
err := s.DB.
|
||||
Order("created_at DESC, id DESC").
|
||||
Limit(limit).
|
||||
Find(&operations).Error
|
||||
|
||||
return operations, err
|
||||
}
|
||||
|
||||
func parsePositiveLimit(raw string, fallback int) int {
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil || value <= 0 {
|
||||
return fallback
|
||||
}
|
||||
|
||||
if value > 200 {
|
||||
return 200
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user