Initial commit: Эфир мессенджер

This commit is contained in:
2026-04-06 14:57:36 +03:00
commit ff93679b6d
50 changed files with 5642 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
package handlers
import (
"messenger/internal/api/responses"
"messenger/internal/service"
"net/http"
//"github.com/go-chi/chi/v5"
)
type AdminHandler struct {
adminService *service.AdminService
}
func NewAdminHandler(adminService *service.AdminService) *AdminHandler {
return &AdminHandler{
adminService: adminService,
}
}
func (h *AdminHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
// userID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
// if err != nil {
// responses.BadRequest(w, "invalid user id")
// return
// }
// TODO: Реализовать удаление пользователя
responses.Success(w, http.StatusOK, map[string]string{"message": "user deleted"})
}
func (h *AdminHandler) DeleteMessage(w http.ResponseWriter, r *http.Request) {
// messageID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
// if err != nil {
// responses.BadRequest(w, "invalid message id")
// return
// }
// TODO: Реализовать удаление сообщения
responses.Success(w, http.StatusOK, map[string]string{"message": "message deleted"})
}

View File

@@ -0,0 +1,80 @@
package handlers
import (
"encoding/json"
"messenger/internal/api/middleware"
"messenger/internal/api/responses"
"messenger/internal/service"
"net/http"
)
type AuthHandler struct {
authService *service.AuthService
}
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
return &AuthHandler{authService: authService}
}
type RegisterRequest struct {
Login string `json:"login"`
Password string `json:"password"`
}
type LoginRequest struct {
Login string `json:"login"`
Password string `json:"password"`
}
type AuthResponse struct {
Token string `json:"token"`
User interface{} `json:"user"`
}
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
var req RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responses.BadRequest(w, "invalid request body")
return
}
user, token, err := h.authService.Register(r.Context(), req.Login, req.Password)
if err != nil {
responses.BadRequest(w, err.Error())
return
}
responses.Success(w, http.StatusCreated, AuthResponse{
Token: token,
User: user.ToSafe(),
})
}
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responses.BadRequest(w, "invalid request body")
return
}
user, token, err := h.authService.Login(r.Context(), req.Login, req.Password)
if err != nil {
responses.Unauthorized(w, err.Error())
return
}
responses.Success(w, http.StatusOK, AuthResponse{
Token: token,
User: user.ToSafe(),
})
}
func (h *AuthHandler) GetMe(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
responses.Success(w, http.StatusOK, user.ToSafe())
}

View File

@@ -0,0 +1,228 @@
package handlers
import (
"encoding/json"
"messenger/internal/api/middleware"
"messenger/internal/api/responses"
"messenger/internal/models"
"messenger/internal/service"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
)
type ChatHandler struct {
chatService *service.ChatService
userService *service.UserService
}
func NewChatHandler(chatService *service.ChatService, userService *service.UserService) *ChatHandler {
return &ChatHandler{
chatService: chatService,
userService: userService,
}
}
func (h *ChatHandler) CreatePrivateChat(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
var req models.CreatePrivateChatRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responses.BadRequest(w, "invalid request body")
return
}
if req.TargetLogin == "" {
responses.BadRequest(w, "target_login is required")
return
}
// Находим целевого пользователя по логину
targetUser, err := h.userService.GetUserByLogin(r.Context(), req.TargetLogin)
if err != nil {
responses.NotFound(w, "target user not found")
return
}
if targetUser == nil {
responses.NotFound(w, "target user not found")
return
}
// Создаем приватный чат
chat, err := h.chatService.CreatePrivateChat(r.Context(), user.ID, targetUser.ID)
if err != nil {
responses.InternalServerError(w, err.Error())
return
}
responses.Success(w, http.StatusCreated, chat)
}
func (h *ChatHandler) CreateGroupChat(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
var req models.CreateGroupChatRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responses.BadRequest(w, "invalid request body")
return
}
chat, err := h.chatService.CreateGroupChat(r.Context(), user.ID, req.Title, req.MemberLogins)
if err != nil {
responses.BadRequest(w, err.Error())
return
}
responses.Success(w, http.StatusCreated, chat)
}
func (h *ChatHandler) GetMyChats(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chats, err := h.chatService.GetUserChatsWithDetails(r.Context(), user.ID)
if err != nil {
responses.InternalServerError(w, err.Error())
return
}
responses.Success(w, http.StatusOK, chats)
}
func (h *ChatHandler) GetChatByID(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chatID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid chat id")
return
}
chat, err := h.chatService.GetChatByID(r.Context(), chatID, user.ID)
if err != nil {
responses.NotFound(w, err.Error())
return
}
responses.Success(w, http.StatusOK, chat)
}
func (h *ChatHandler) GetChatMembers(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chatID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid chat id")
return
}
members, err := h.chatService.GetChatMembers(r.Context(), chatID, user.ID)
if err != nil {
responses.Forbidden(w, err.Error())
return
}
responses.Success(w, http.StatusOK, members)
}
func (h *ChatHandler) AddMembers(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chatID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid chat id")
return
}
var req models.AddMembersRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responses.BadRequest(w, "invalid request body")
return
}
if err := h.chatService.AddMembers(r.Context(), chatID, user.ID, req.UserLogins); err != nil {
responses.BadRequest(w, err.Error())
return
}
responses.Success(w, http.StatusOK, map[string]string{"message": "members added"})
}
func (h *ChatHandler) RemoveMember(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chatID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid chat id")
return
}
memberID, err := strconv.ParseInt(chi.URLParam(r, "user_id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid member id")
return
}
if err := h.chatService.RemoveMember(r.Context(), chatID, user.ID, memberID); err != nil {
responses.BadRequest(w, err.Error())
return
}
responses.Success(w, http.StatusOK, map[string]string{"message": "member removed"})
}
func (h *ChatHandler) UpdateChatTitle(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chatID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid chat id")
return
}
var req models.UpdateChatTitleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responses.BadRequest(w, "invalid request body")
return
}
if err := h.chatService.UpdateChatTitle(r.Context(), chatID, user.ID, req.Title); err != nil {
responses.BadRequest(w, err.Error())
return
}
responses.Success(w, http.StatusOK, map[string]string{"message": "chat title updated"})
}

View File

@@ -0,0 +1,82 @@
package handlers
import (
"messenger/internal/api/middleware"
"messenger/internal/api/responses"
"messenger/internal/service"
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5"
)
type FileHandler struct {
fileService *service.FileService
}
func NewFileHandler(fileService *service.FileService) *FileHandler {
return &FileHandler{
fileService: fileService,
}
}
func (h *FileHandler) UploadFile(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chatID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid chat id")
return
}
// Parse multipart form (20 MB max)
if err := r.ParseMultipartForm(20 << 20); err != nil {
responses.BadRequest(w, "failed to parse form")
return
}
file, header, err := r.FormFile("file")
if err != nil {
responses.BadRequest(w, "file is required")
return
}
defer file.Close()
attachment, err := h.fileService.UploadFile(r.Context(), chatID, user.ID, header)
if err != nil {
responses.BadRequest(w, err.Error())
return
}
responses.Success(w, http.StatusOK, attachment)
}
func (h *FileHandler) DownloadFile(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
attachmentID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid attachment id")
return
}
fileName, file, mimeType, err := h.fileService.DownloadFile(r.Context(), attachmentID, user.ID)
if err != nil {
responses.NotFound(w, err.Error())
return
}
defer file.Close()
w.Header().Set("Content-Type", mimeType)
w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(fileName))
http.ServeContent(w, r, fileName, time.Now(), file)
}

View File

@@ -0,0 +1,83 @@
package handlers
import (
//"encoding/json"
"messenger/internal/api/middleware"
"messenger/internal/api/responses"
//"messenger/internal/models"
"messenger/internal/service"
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5"
)
type MessageHandler struct {
messageService *service.MessageService
chatService *service.ChatService
}
func NewMessageHandler(messageService *service.MessageService, chatService *service.ChatService) *MessageHandler {
return &MessageHandler{
messageService: messageService,
chatService: chatService,
}
}
func (h *MessageHandler) GetMessages(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
chatID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid chat id")
return
}
limit := 50
if l := r.URL.Query().Get("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
limit = parsed
}
}
var before time.Time
if b := r.URL.Query().Get("before"); b != "" {
if parsed, err := time.Parse(time.RFC3339, b); err == nil {
before = parsed
}
}
messages, err := h.messageService.GetChatHistory(r.Context(), chatID, user.ID, limit, before)
if err != nil {
responses.InternalServerError(w, err.Error())
return
}
responses.Success(w, http.StatusOK, messages)
}
func (h *MessageHandler) MarkAsRead(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
messageID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
responses.BadRequest(w, "invalid message id")
return
}
if err := h.messageService.MarkMessageAsRead(r.Context(), messageID, user.ID); err != nil {
responses.InternalServerError(w, err.Error())
return
}
responses.Success(w, http.StatusOK, map[string]string{"message": "marked as read"})
}

View File

@@ -0,0 +1,103 @@
package handlers
import (
"context"
"encoding/json"
"messenger/internal/api/middleware"
"messenger/internal/api/responses"
"messenger/internal/models"
"messenger/internal/service"
"net/http"
"strconv"
)
type UserHandler struct {
userService *service.UserService
}
func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
type UpdateProfileRequest struct {
DisplayName *string `json:"display_name,omitempty"`
Bio *string `json:"bio,omitempty"`
}
func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
profile, err := h.userService.GetProfile(r.Context(), user.ID)
if err != nil {
responses.NotFound(w, err.Error())
return
}
responses.Success(w, http.StatusOK, profile)
}
func (h *UserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUserFromContext(r.Context())
if user == nil {
responses.Unauthorized(w, "user not found")
return
}
var req UpdateProfileRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
responses.BadRequest(w, "invalid request body")
return
}
if err := h.userService.UpdateProfile(r.Context(), user.ID, req.DisplayName, req.Bio); err != nil {
responses.BadRequest(w, err.Error())
return
}
responses.Success(w, http.StatusOK, map[string]string{"message": "profile updated"})
}
func (h *UserHandler) SearchUsers(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
responses.BadRequest(w, "search query required")
return
}
limit := 20
if l := r.URL.Query().Get("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
limit = parsed
}
}
users, err := h.userService.SearchUsers(r.Context(), query, limit)
if err != nil {
responses.InternalServerError(w, err.Error())
return
}
responses.Success(w, http.StatusOK, users)
}
func (h *UserHandler) GetUserByID(w http.ResponseWriter, r *http.Request) {
// Реализация с chi.URLParam будет в main.go
// Пока заглушка
responses.Success(w, http.StatusOK, nil)
}
// GetUserByLoginFromContext - метод для получения пользователя по логину (используется в других хендлерах)
func (h *UserHandler) GetUserByLogin(ctx context.Context, login string) (*models.SafeUser, error) {
user, err := h.userService.GetUserByLogin(ctx, login)
if err != nil {
return nil, err
}
if user == nil {
return nil, nil
}
return user.ToSafe(), nil
}

View File

@@ -0,0 +1,58 @@
package handlers
import (
"net/http"
gorillaWS "github.com/gorilla/websocket"
"messenger/internal/pkg/logger"
"messenger/internal/service"
ws "messenger/internal/websocket"
)
var upgrader = gorillaWS.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type WebSocketHandler struct {
hub *ws.Hub
authService *service.AuthService
}
func NewWebSocketHandler(hub *ws.Hub, authService *service.AuthService) *WebSocketHandler {
return &WebSocketHandler{
hub: hub,
authService: authService,
}
}
func (h *WebSocketHandler) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if token == "" {
http.Error(w, "missing token", http.StatusUnauthorized)
return
}
user, err := h.authService.ValidateToken(token)
if err != nil {
logger.Error("WebSocket auth failed", "error", err)
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
logger.Info("WebSocket connecting", "user_id", user.ID, "login", user.Login)
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
logger.Error("WebSocket upgrade failed", "error", err)
return
}
client := ws.NewClient(h.hub, conn, user)
h.hub.GetRegisterChan() <- client
go client.WritePump()
client.ReadPump()
}

View File

@@ -0,0 +1,66 @@
package middleware
import (
"context"
"messenger/internal/api/responses"
"messenger/internal/models"
"messenger/internal/service"
"net/http"
"strings"
)
type contextKey string
const UserContextKey contextKey = "user"
func JWTAuth(authService *service.AuthService) func(next 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 == "" {
responses.Unauthorized(w, "missing authorization header")
return
}
// Проверяем формат Bearer token
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
responses.Unauthorized(w, "invalid authorization header format")
return
}
token := parts[1]
// Валидируем токен
user, err := authService.ValidateToken(token)
if err != nil {
responses.Unauthorized(w, "invalid or expired token")
return
}
// Сохраняем пользователя в контексте
ctx := context.WithValue(r.Context(), UserContextKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func GetUserFromContext(ctx context.Context) *models.User {
user, ok := ctx.Value(UserContextKey).(*models.User)
if !ok {
return nil
}
return user
}
func RequireGlobalAdmin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := GetUserFromContext(r.Context())
if user == nil || !user.IsGlobalAdmin() {
responses.Forbidden(w, "global admin access required")
return
}
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,38 @@
package middleware
import (
"net/http"
//"strings"
)
func CORS(allowedOrigins []string) func(next 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")
// Проверяем разрешен ли origin
allowed := false
for _, o := range allowedOrigins {
if o == "*" || o == 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, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
}

View File

@@ -0,0 +1,36 @@
package middleware
import (
"log/slog"
"net/http"
"time"
)
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Оборачиваем ResponseWriter для захвата статуса
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
slog.Info("HTTP request",
"method", r.Method,
"path", r.URL.Path,
"status", wrapped.statusCode,
"duration", time.Since(start),
"remote_addr", r.RemoteAddr,
)
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}

View File

@@ -0,0 +1,23 @@
package middleware
import (
"log/slog"
"net/http"
"runtime/debug"
)
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
slog.Error("panic recovered",
"error", err,
"stack", string(debug.Stack()),
"path", r.URL.Path,
)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,54 @@
package responses
import (
"encoding/json"
"net/http"
)
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Code int `json:"code,omitempty"`
}
func JSON(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
func Success(w http.ResponseWriter, statusCode int, data interface{}) {
JSON(w, statusCode, Response{
Success: true,
Data: data,
})
}
func Error(w http.ResponseWriter, statusCode int, errMsg string) {
JSON(w, statusCode, Response{
Success: false,
Error: errMsg,
Code: statusCode,
})
}
func BadRequest(w http.ResponseWriter, errMsg string) {
Error(w, http.StatusBadRequest, errMsg)
}
func Unauthorized(w http.ResponseWriter, errMsg string) {
Error(w, http.StatusUnauthorized, errMsg)
}
func Forbidden(w http.ResponseWriter, errMsg string) {
Error(w, http.StatusForbidden, errMsg)
}
func NotFound(w http.ResponseWriter, errMsg string) {
Error(w, http.StatusNotFound, errMsg)
}
func InternalServerError(w http.ResponseWriter, errMsg string) {
Error(w, http.StatusInternalServerError, errMsg)
}