initial commit

This commit is contained in:
2025-11-29 00:28:21 +05:00
parent 46229acc82
commit ec3b03a935
76 changed files with 13492 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
package handlers
import (
"encoding/json"
"net/http"
"git.kirlllll.ru/volontery/backend/internal/api/middleware"
"git.kirlllll.ru/volontery/backend/internal/service"
)
// AuthHandler обрабатывает запросы аутентификации
type AuthHandler struct {
authService *service.AuthService
}
// NewAuthHandler создает новый AuthHandler
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
return &AuthHandler{
authService: authService,
}
}
// Register обрабатывает регистрацию пользователя
// POST /api/v1/auth/register
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
var req service.RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
resp, err := h.authService.Register(r.Context(), req)
if err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
respondJSON(w, http.StatusCreated, resp)
}
// Login обрабатывает вход пользователя
// POST /api/v1/auth/login
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
var req service.LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
resp, err := h.authService.Login(r.Context(), req)
if err != nil {
respondError(w, http.StatusUnauthorized, err.Error())
return
}
respondJSON(w, http.StatusOK, resp)
}
// RefreshToken обрабатывает обновление токенов
// POST /api/v1/auth/refresh
func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
var req struct {
RefreshToken string `json:"refresh_token"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.RefreshToken == "" {
respondError(w, http.StatusBadRequest, "refresh_token is required")
return
}
resp, err := h.authService.RefreshTokens(r.Context(), req.RefreshToken)
if err != nil {
respondError(w, http.StatusUnauthorized, err.Error())
return
}
respondJSON(w, http.StatusOK, resp)
}
// Logout обрабатывает выход пользователя
// POST /api/v1/auth/logout
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
if err := h.authService.Logout(r.Context(), userID); err != nil {
respondError(w, http.StatusInternalServerError, "failed to logout")
return
}
respondJSON(w, http.StatusOK, map[string]string{"message": "logged out successfully"})
}
// Me возвращает информацию о текущем пользователе
// GET /api/v1/auth/me
func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
email, _ := middleware.GetUserEmailFromContext(r.Context())
respondJSON(w, http.StatusOK, map[string]interface{}{
"id": userID,
"email": email,
})
}

View File

@@ -0,0 +1,28 @@
package handlers
import (
"encoding/json"
"net/http"
)
// ErrorResponse представляет ответ с ошибкой
type ErrorResponse struct {
Error string `json:"error"`
}
// respondJSON отправляет JSON ответ
func respondJSON(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if data != nil {
if err := json.NewEncoder(w).Encode(data); err != nil {
http.Error(w, "failed to encode response", http.StatusInternalServerError)
}
}
}
// respondError отправляет ошибку в формате JSON
func respondError(w http.ResponseWriter, statusCode int, message string) {
respondJSON(w, statusCode, ErrorResponse{Error: message})
}

View File

@@ -0,0 +1,460 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"git.kirlllll.ru/volontery/backend/internal/api/middleware"
"git.kirlllll.ru/volontery/backend/internal/database"
"git.kirlllll.ru/volontery/backend/internal/service"
"github.com/go-chi/chi/v5"
)
// RequestHandler обрабатывает запросы заявок
type RequestHandler struct {
requestService *service.RequestService
}
// NewRequestHandler создает новый RequestHandler
func NewRequestHandler(requestService *service.RequestService) *RequestHandler {
return &RequestHandler{
requestService: requestService,
}
}
// CreateRequest создает новую заявку
// POST /api/v1/requests
func (h *RequestHandler) CreateRequest(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
var input service.CreateRequestInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
input.RequesterID = userID
request, err := h.requestService.CreateRequest(r.Context(), input)
if err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
respondJSON(w, http.StatusCreated, request)
}
// GetRequest получает заявку по ID
// GET /api/v1/requests/{id}
func (h *RequestHandler) GetRequest(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
requestID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid request id")
return
}
request, err := h.requestService.GetRequest(r.Context(), requestID)
if err != nil {
respondError(w, http.StatusNotFound, "request not found")
return
}
respondJSON(w, http.StatusOK, request)
}
// GetMyRequests получает заявки текущего пользователя
// GET /api/v1/requests/my
func (h *RequestHandler) GetMyRequests(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
limit, offset := parsePagination(r)
requests, err := h.requestService.GetUserRequests(r.Context(), userID, limit, offset)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to get requests")
return
}
respondJSON(w, http.StatusOK, requests)
}
// FindNearbyRequests ищет заявки рядом с точкой
// GET /api/v1/requests/nearby
func (h *RequestHandler) FindNearbyRequests(w http.ResponseWriter, r *http.Request) {
latStr := r.URL.Query().Get("lat")
lonStr := r.URL.Query().Get("lon")
radiusStr := r.URL.Query().Get("radius")
if latStr == "" || lonStr == "" {
respondError(w, http.StatusBadRequest, "lat and lon are required")
return
}
lat, err := strconv.ParseFloat(latStr, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid latitude")
return
}
lon, err := strconv.ParseFloat(lonStr, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid longitude")
return
}
radius := 5000.0 // default 5km
if radiusStr != "" {
radius, err = strconv.ParseFloat(radiusStr, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid radius")
return
}
}
limit, offset := parsePagination(r)
// По умолчанию ищем только активные заявки
statuses := []database.RequestStatus{
database.RequestStatusPendingModeration,
database.RequestStatusApproved,
database.RequestStatusInProgress,
}
requests, err := h.requestService.FindNearbyRequests(r.Context(), lat, lon, radius, statuses, limit, offset)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to find requests: %v", err))
return
}
respondJSON(w, http.StatusOK, requests)
}
// FindRequestsInBounds ищет заявки в прямоугольной области
// GET /api/v1/requests/bounds
func (h *RequestHandler) FindRequestsInBounds(w http.ResponseWriter, r *http.Request) {
minLonStr := r.URL.Query().Get("min_lon")
minLatStr := r.URL.Query().Get("min_lat")
maxLonStr := r.URL.Query().Get("max_lon")
maxLatStr := r.URL.Query().Get("max_lat")
if minLonStr == "" || minLatStr == "" || maxLonStr == "" || maxLatStr == "" {
respondError(w, http.StatusBadRequest, "min_lon, min_lat, max_lon, max_lat are required")
return
}
minLon, err := strconv.ParseFloat(minLonStr, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid min_lon")
return
}
minLat, err := strconv.ParseFloat(minLatStr, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid min_lat")
return
}
maxLon, err := strconv.ParseFloat(maxLonStr, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid max_lon")
return
}
maxLat, err := strconv.ParseFloat(maxLatStr, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid max_lat")
return
}
// По умолчанию ищем только активные заявки
statuses := []database.RequestStatus{
database.RequestStatusPendingModeration,
database.RequestStatusApproved,
database.RequestStatusInProgress,
}
requests, err := h.requestService.FindRequestsInBounds(r.Context(), statuses, minLon, minLat, maxLon, maxLat)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to find requests: %v", err))
return
}
respondJSON(w, http.StatusOK, requests)
}
// CreateVolunteerResponse создает отклик волонтера на заявку
// POST /api/v1/requests/{id}/responses
func (h *RequestHandler) CreateVolunteerResponse(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
idStr := chi.URLParam(r, "id")
requestID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid request id")
return
}
var input struct {
Message string `json:"message"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
response, err := h.requestService.CreateVolunteerResponse(r.Context(), requestID, userID, input.Message)
if err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
respondJSON(w, http.StatusCreated, response)
}
// GetRequestResponses получает отклики на заявку
// GET /api/v1/requests/{id}/responses
func (h *RequestHandler) GetRequestResponses(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
requestID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid request id")
return
}
responses, err := h.requestService.GetRequestResponses(r.Context(), requestID)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to get responses")
return
}
respondJSON(w, http.StatusOK, responses)
}
// ListRequestTypes получает список типов заявок
// GET /api/v1/request-types
func (h *RequestHandler) ListRequestTypes(w http.ResponseWriter, r *http.Request) {
types, err := h.requestService.ListRequestTypes(r.Context())
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to get request types")
return
}
respondJSON(w, http.StatusOK, types)
}
// GetPendingModerationRequests получает заявки на модерации
// GET /api/v1/moderation/requests/pending
func (h *RequestHandler) GetPendingModerationRequests(w http.ResponseWriter, r *http.Request) {
limit, offset := parsePagination(r)
requests, err := h.requestService.GetPendingModerationRequests(r.Context(), limit, offset)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to get pending requests")
return
}
respondJSON(w, http.StatusOK, requests)
}
// ApproveRequest одобряет заявку
// POST /api/v1/moderation/requests/{id}/approve
func (h *RequestHandler) ApproveRequest(w http.ResponseWriter, r *http.Request) {
moderatorID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
idStr := chi.URLParam(r, "id")
requestID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid request id")
return
}
var input struct {
Comment *string `json:"comment"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := h.requestService.ApproveRequest(r.Context(), requestID, moderatorID, input.Comment); err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
respondJSON(w, http.StatusOK, map[string]string{"status": "approved"})
}
// RejectRequest отклоняет заявку
// POST /api/v1/moderation/requests/{id}/reject
func (h *RequestHandler) RejectRequest(w http.ResponseWriter, r *http.Request) {
moderatorID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
idStr := chi.URLParam(r, "id")
requestID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid request id")
return
}
var input struct {
Comment string `json:"comment"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := h.requestService.RejectRequest(r.Context(), requestID, moderatorID, input.Comment); err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
respondJSON(w, http.StatusOK, map[string]string{"status": "rejected"})
}
// GetMyModeratedRequests получает заявки, модерированные текущим модератором
// GET /api/v1/moderation/requests/my
func (h *RequestHandler) GetMyModeratedRequests(w http.ResponseWriter, r *http.Request) {
moderatorID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
limit, offset := parsePagination(r)
requests, err := h.requestService.GetModeratedRequests(r.Context(), moderatorID, limit, offset)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to get moderated requests")
return
}
respondJSON(w, http.StatusOK, requests)
}
// AcceptVolunteerResponseHandler принимает отклик волонтера
// POST /api/v1/requests/{id}/responses/{response_id}/accept
func (h *RequestHandler) AcceptVolunteerResponseHandler(w http.ResponseWriter, r *http.Request) {
requesterID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
responseIDStr := chi.URLParam(r, "response_id")
responseID, err := strconv.ParseInt(responseIDStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid response id")
return
}
result, err := h.requestService.AcceptVolunteerResponse(r.Context(), responseID, requesterID)
if err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
if !result.Success {
respondError(w, http.StatusBadRequest, result.Message)
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"success": result.Success,
"message": result.Message,
"request_id": result.RequestID,
"volunteer_id": result.VolunteerID,
})
}
// CompleteRequestWithRatingHandler завершает заявку с рейтингом
// POST /api/v1/requests/{id}/complete
func (h *RequestHandler) CompleteRequestWithRatingHandler(w http.ResponseWriter, r *http.Request) {
requesterID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
idStr := chi.URLParam(r, "id")
requestID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid request id")
return
}
var input struct {
Rating int32 `json:"rating"`
Comment *string `json:"comment"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
result, err := h.requestService.CompleteRequestWithRating(r.Context(), requestID, requesterID, input.Rating, input.Comment)
if err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
if !result.Success {
respondError(w, http.StatusBadRequest, result.Message)
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"success": result.Success,
"message": result.Message,
"rating_id": result.RatingID,
})
}
// parsePagination парсит параметры пагинации из запроса
func parsePagination(r *http.Request) (limit, offset int32) {
limitStr := r.URL.Query().Get("limit")
offsetStr := r.URL.Query().Get("offset")
limit = 20 // default
if limitStr != "" {
if l, err := strconv.ParseInt(limitStr, 10, 32); err == nil && l > 0 && l <= 100 {
limit = int32(l)
}
}
offset = 0
if offsetStr != "" {
if o, err := strconv.ParseInt(offsetStr, 10, 32); err == nil && o >= 0 {
offset = int32(o)
}
}
return limit, offset
}

View File

@@ -0,0 +1,308 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"git.kirlllll.ru/volontery/backend/internal/api/middleware"
"git.kirlllll.ru/volontery/backend/internal/service"
"github.com/go-chi/chi/v5"
)
// UserHandler обрабатывает запросы пользователей
type UserHandler struct {
userService *service.UserService
}
// NewUserHandler создает новый UserHandler
func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{
userService: userService,
}
}
// GetProfile возвращает профиль пользователя
// GET /api/v1/users/{id}
func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
userID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid user id")
return
}
profile, err := h.userService.GetUserProfile(r.Context(), userID)
if err != nil {
respondError(w, http.StatusNotFound, "user not found")
return
}
respondJSON(w, http.StatusOK, profile)
}
// GetMyProfile возвращает профиль текущего пользователя
// GET /api/v1/users/me
func (h *UserHandler) GetMyProfile(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
profile, err := h.userService.GetUserProfile(r.Context(), userID)
if err != nil {
respondError(w, http.StatusNotFound, "user not found")
return
}
respondJSON(w, http.StatusOK, profile)
}
// UpdateProfile обновляет профиль текущего пользователя
// PATCH /api/v1/users/me
func (h *UserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
var input service.UpdateProfileInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := h.userService.UpdateUserProfile(r.Context(), userID, input); err != nil {
respondError(w, http.StatusInternalServerError, "failed to update profile")
return
}
respondJSON(w, http.StatusOK, map[string]string{"message": "profile updated successfully"})
}
// UpdateLocation обновляет местоположение пользователя
// POST /api/v1/users/me/location
func (h *UserHandler) UpdateLocation(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
var input struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := h.userService.UpdateUserLocation(r.Context(), userID, input.Latitude, input.Longitude); err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
respondJSON(w, http.StatusOK, map[string]string{"message": "location updated successfully"})
}
// GetMyRoles возвращает роли текущего пользователя
// GET /api/v1/users/me/roles
func (h *UserHandler) GetMyRoles(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
roles, err := h.userService.GetUserRoles(r.Context(), userID)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to get roles")
return
}
respondJSON(w, http.StatusOK, roles)
}
// GetMyPermissions возвращает разрешения текущего пользователя
// GET /api/v1/users/me/permissions
func (h *UserHandler) GetMyPermissions(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
permissions, err := h.userService.GetUserPermissions(r.Context(), userID)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to get permissions")
return
}
respondJSON(w, http.StatusOK, permissions)
}
// VerifyEmail подтверждает email пользователя
// POST /api/v1/users/me/verify-email
func (h *UserHandler) VerifyEmail(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
// В реальной системе здесь должна быть проверка токена из письма
// Сейчас просто подтверждаем email для текущего пользователя
if err := h.userService.VerifyEmail(r.Context(), userID); err != nil {
respondError(w, http.StatusInternalServerError, "failed to verify email")
return
}
respondJSON(w, http.StatusOK, map[string]string{"message": "email verified successfully"})
}
// CheckPermission проверяет наличие разрешения у пользователя
// GET /api/v1/users/me/permissions/{permission_name}/check
func (h *UserHandler) CheckPermission(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
permissionName := chi.URLParam(r, "permission_name")
if permissionName == "" {
respondError(w, http.StatusBadRequest, "permission_name is required")
return
}
hasPermission, err := h.userService.HasPermission(r.Context(), userID, permissionName)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to check permission")
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"has_permission": hasPermission,
"permission_name": permissionName,
})
}
// ========== AdminHandler - новый хендлер для административных функций ==========
// AdminHandler обрабатывает административные запросы
type AdminHandler struct {
userService *service.UserService
}
// NewAdminHandler создает новый AdminHandler
func NewAdminHandler(userService *service.UserService) *AdminHandler {
return &AdminHandler{
userService: userService,
}
}
// AssignRole назначает роль пользователю
// POST /api/v1/admin/users/{user_id}/roles/{role_id}
func (h *AdminHandler) AssignRole(w http.ResponseWriter, r *http.Request) {
adminID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
// Проверка, что текущий пользователь является администратором
// В реальной системе это должно быть в middleware
isAdmin, err := h.userService.HasPermission(r.Context(), adminID, "manage_users")
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to check permissions")
return
}
if !isAdmin {
respondError(w, http.StatusForbidden, "admin role required")
return
}
userIDStr := chi.URLParam(r, "user_id")
userID, err := strconv.ParseInt(userIDStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid user_id")
return
}
roleIDStr := chi.URLParam(r, "role_id")
roleID, err := strconv.ParseInt(roleIDStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid role_id")
return
}
if err := h.userService.AssignRole(r.Context(), userID, roleID, adminID); err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"message": "role assigned successfully",
"user_id": userID,
"role_id": roleID,
})
}
// ========== RequestHandler - дополнительный метод ==========
// ModerateRequestProcedure модерирует заявку через stored procedure
// POST /api/v1/moderation/requests/{id}/moderate
func (h *RequestHandler) ModerateRequestProcedure(w http.ResponseWriter, r *http.Request) {
moderatorID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
idStr := chi.URLParam(r, "id")
requestID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid request id")
return
}
var input struct {
Action string `json:"action"`
Comment *string `json:"comment"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
if input.Action != "approve" && input.Action != "reject" {
respondError(w, http.StatusBadRequest, "action must be 'approve' or 'reject'")
return
}
if input.Action == "reject" && (input.Comment == nil || *input.Comment == "") {
respondError(w, http.StatusBadRequest, "comment is required when rejecting")
return
}
result, err := h.requestService.ModerateRequestProcedure(r.Context(), requestID, moderatorID, input.Action, input.Comment)
if err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
if !result.Success {
respondError(w, http.StatusBadRequest, result.Message)
return
}
respondJSON(w, http.StatusOK, map[string]interface{}{
"success": result.Success,
"message": result.Message,
})
}

25
internal/api/helpers.go Normal file
View File

@@ -0,0 +1,25 @@
package api
import (
"encoding/json"
"net/http"
)
// ErrorResponse represents a JSON error response
type ErrorResponse struct {
Error string `json:"error"`
}
// JSONError sends a JSON error response
func JSONError(w http.ResponseWriter, message string, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(ErrorResponse{Error: message})
}
// JSONResponse sends a JSON response
func JSONResponse(w http.ResponseWriter, data interface{}, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}

View File

@@ -0,0 +1,107 @@
package middleware
import (
"context"
"encoding/json"
"net/http"
"strings"
"git.kirlllll.ru/volontery/backend/internal/pkg/jwt"
)
type contextKey string
const (
UserIDKey contextKey = "user_id"
UserEmailKey contextKey = "user_email"
)
// ErrorResponse represents a JSON error response
type ErrorResponse struct {
Error string `json:"error"`
}
// JSONError sends a JSON error response
func JSONError(w http.ResponseWriter, message string, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(ErrorResponse{Error: message})
}
// AuthMiddleware создает middleware для проверки JWT токена
func AuthMiddleware(jwtManager *jwt.Manager) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Получение токена из заголовка Authorization
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
JSONError(w, "authorization header required", http.StatusUnauthorized)
return
}
// Проверка формата "Bearer <token>"
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
JSONError(w, "invalid authorization header format", http.StatusUnauthorized)
return
}
tokenString := parts[1]
// Валидация токена
claims, err := jwtManager.ValidateToken(tokenString)
if err != nil {
JSONError(w, "invalid or expired token", http.StatusUnauthorized)
return
}
// Добавление данных пользователя в контекст
ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID)
ctx = context.WithValue(ctx, UserEmailKey, claims.Email)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// GetUserIDFromContext извлекает ID пользователя из контекста
func GetUserIDFromContext(ctx context.Context) (int64, bool) {
userID, ok := ctx.Value(UserIDKey).(int64)
return userID, ok
}
// GetUserEmailFromContext извлекает email пользователя из контекста
func GetUserEmailFromContext(ctx context.Context) (string, bool) {
email, ok := ctx.Value(UserEmailKey).(string)
return email, ok
}
// OptionalAuthMiddleware делает аутентификацию опциональной (не возвращает ошибку если токена нет)
func OptionalAuthMiddleware(jwtManager *jwt.Manager) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
next.ServeHTTP(w, r)
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
next.ServeHTTP(w, r)
return
}
claims, err := jwtManager.ValidateToken(parts[1])
if err != nil {
next.ServeHTTP(w, r)
return
}
ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID)
ctx = context.WithValue(ctx, UserEmailKey, claims.Email)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

View File

@@ -0,0 +1,94 @@
package middleware
import (
"log"
"net/http"
"runtime/debug"
"time"
)
// Logger middleware для логирования HTTP запросов
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Обертка для захвата статус кода
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
log.Printf(
"%s %s %d %s",
r.Method,
r.RequestURI,
wrapped.statusCode,
time.Since(start),
)
})
}
// responseWriter обертка для захвата статус кода
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// Recovery middleware для восстановления после паник
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v\n%s", err, debug.Stack())
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// CORS middleware для настройки CORS заголовков
func CORS(allowedOrigins []string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Проверка разрешенных источников
allowed := false
for _, allowedOrigin := range allowedOrigins {
if allowedOrigin == "*" || allowedOrigin == origin {
allowed = true
break
}
}
if allowed {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Max-Age", "86400")
}
// Обработка preflight запросов
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
// ContentTypeJSON устанавливает Content-Type: application/json
func ContentTypeJSON(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,106 @@
package middleware
import (
"net/http"
"git.kirlllll.ru/volontery/backend/internal/service"
)
// RequirePermission создает middleware для проверки разрешения
func RequirePermission(userService *service.UserService, permission string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID, ok := GetUserIDFromContext(r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
hasPermission, err := userService.HasPermission(r.Context(), userID, permission)
if err != nil {
http.Error(w, "failed to check permissions", http.StatusInternalServerError)
return
}
if !hasPermission {
http.Error(w, "forbidden: insufficient permissions", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
// RequireRole создает middleware для проверки роли
func RequireRole(userService *service.UserService, roleName string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID, ok := GetUserIDFromContext(r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
roles, err := userService.GetUserRoles(r.Context(), userID)
if err != nil {
http.Error(w, "failed to check roles", http.StatusInternalServerError)
return
}
hasRole := false
for _, role := range roles {
if role.Name == roleName {
hasRole = true
break
}
}
if !hasRole {
http.Error(w, "forbidden: required role not assigned", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
// RequireAnyRole создает middleware для проверки наличия хотя бы одной из ролей
func RequireAnyRole(userService *service.UserService, roleNames ...string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID, ok := GetUserIDFromContext(r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
roles, err := userService.GetUserRoles(r.Context(), userID)
if err != nil {
http.Error(w, "failed to check roles", http.StatusInternalServerError)
return
}
hasAnyRole := false
for _, role := range roles {
for _, requiredRole := range roleNames {
if role.Name == requiredRole {
hasAnyRole = true
break
}
}
if hasAnyRole {
break
}
}
if !hasAnyRole {
http.Error(w, "forbidden: none of the required roles assigned", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}

140
internal/api/router.go Normal file
View File

@@ -0,0 +1,140 @@
package api
import (
"net/http"
"git.kirlllll.ru/volontery/backend/internal/api/handlers"
"git.kirlllll.ru/volontery/backend/internal/api/middleware"
"git.kirlllll.ru/volontery/backend/internal/config"
"git.kirlllll.ru/volontery/backend/internal/pkg/jwt"
"git.kirlllll.ru/volontery/backend/internal/service"
"github.com/go-chi/chi/v5"
chiMiddleware "github.com/go-chi/chi/v5/middleware"
)
// Server представляет HTTP сервер
type Server struct {
router *chi.Mux
authHandler *handlers.AuthHandler
adminHandler *handlers.AdminHandler
userHandler *handlers.UserHandler
requestHandler *handlers.RequestHandler
jwtManager *jwt.Manager
userService *service.UserService
config *config.Config
}
// NewServer создает новый HTTP сервер
func NewServer(
cfg *config.Config,
authService *service.AuthService,
userService *service.UserService,
requestService *service.RequestService,
jwtManager *jwt.Manager,
) *Server {
s := &Server{
router: chi.NewRouter(),
authHandler: handlers.NewAuthHandler(authService),
adminHandler: handlers.NewAdminHandler(userService),
userHandler: handlers.NewUserHandler(userService),
requestHandler: handlers.NewRequestHandler(requestService),
jwtManager: jwtManager,
userService: userService,
config: cfg,
}
s.setupRoutes()
return s
}
// setupRoutes настраивает маршруты
func (s *Server) setupRoutes() {
r := s.router
// Глобальные middleware
r.Use(chiMiddleware.RequestID)
r.Use(chiMiddleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recovery)
r.Use(middleware.CORS(s.config.CORSAllowedOrigins))
// Health check
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
// API v1
r.Route("/api/v1", func(r chi.Router) {
// Публичные маршруты (без аутентификации)
r.Group(func(r chi.Router) {
r.Post("/auth/register", s.authHandler.Register)
r.Post("/auth/login", s.authHandler.Login)
r.Post("/auth/refresh", s.authHandler.RefreshToken)
// Типы заявок (публичные)
r.Get("/request-types", s.requestHandler.ListRequestTypes)
})
// Защищенные маршруты (требуют аутентификации)
r.Group(func(r chi.Router) {
r.Use(middleware.AuthMiddleware(s.jwtManager))
// Auth
r.Get("/auth/me", s.authHandler.Me)
r.Post("/auth/logout", s.authHandler.Logout)
// Users
r.Get("/users/me", s.userHandler.GetMyProfile)
r.Patch("/users/me", s.userHandler.UpdateProfile)
r.Post("/users/me/location", s.userHandler.UpdateLocation)
r.Post("/users/me/verify-email", s.userHandler.VerifyEmail)
r.Get("/users/me/roles", s.userHandler.GetMyRoles)
r.Get("/users/me/permissions", s.userHandler.GetMyPermissions)
r.Get("/users/{id}", s.userHandler.GetProfile)
// Requests
r.Post("/requests", s.requestHandler.CreateRequest)
r.Get("/requests/my", s.requestHandler.GetMyRequests)
r.Get("/requests/nearby", s.requestHandler.FindNearbyRequests)
r.Get("/requests/bounds", s.requestHandler.FindRequestsInBounds)
r.Get("/requests/{id}", s.requestHandler.GetRequest)
r.Post("/requests/{id}/responses", s.requestHandler.CreateVolunteerResponse)
r.Get("/requests/{id}/responses", s.requestHandler.GetRequestResponses)
r.Post("/requests/{id}/responses/{response_id}/accept", s.requestHandler.AcceptVolunteerResponseHandler)
r.Post("/requests/{id}/complete", s.requestHandler.CompleteRequestWithRatingHandler)
})
// Маршруты для модераторов (требуют роль moderator или admin)
r.Group(func(r chi.Router) {
r.Use(middleware.AuthMiddleware(s.jwtManager))
r.Use(middleware.RequireAnyRole(s.userService, "moderator", "admin"))
// Модерация заявок
r.Get("/moderation/requests/pending", s.requestHandler.GetPendingModerationRequests)
r.Get("/moderation/requests/my", s.requestHandler.GetMyModeratedRequests)
r.Post("/moderation/requests/{id}/approve", s.requestHandler.ApproveRequest)
r.Post("/moderation/requests/{id}/reject", s.requestHandler.RejectRequest)
r.Post("/moderation/requests/{id}/moderate", s.requestHandler.ModerateRequestProcedure)
})
// Маршруты для администраторов (требуют роль admin)
r.Group(func(r chi.Router) {
r.Use(middleware.AuthMiddleware(s.jwtManager))
r.Use(middleware.RequireRole(s.userService, "admin"))
// Админские маршруты можно добавить позже
r.Get("/admin/users/{id}/roles/{role_id}", s.adminHandler.AssignRole)
})
})
}
// ServeHTTP реализует интерфейс http.Handler
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}
// Router возвращает роутер Chi
func (s *Server) Router() *chi.Mux {
return s.router
}