package app import ( "context" "crypto/sha256" "encoding/base64" "errors" "fmt" "net/netip" "strconv" "strings" "time" "evening_detective_server/docs" "evening_detective_server/internal/modules/file_storage" "evening_detective_server/internal/modules/processor_jwt" "evening_detective_server/internal/modules/roles" "evening_detective_server/internal/modules/scenario_archive" "evening_detective_server/internal/modules/string_tools" "evening_detective_server/internal/repos/game_map_repo" "evening_detective_server/internal/repos/scenarios_repo" "evening_detective_server/internal/repos/screen_sessions_repo" "evening_detective_server/internal/repos/users_repo" "evening_detective_server/internal/services/file_service" "evening_detective_server/internal/services/game_service" "evening_detective_server/internal/services/scenarios_service" "evening_detective_server/internal/services/ui_service" "evening_detective_server/internal/services/users_service" proto "evening_detective_server/proto" "github.com/google/uuid" "google.golang.org/genproto/googleapis/api/httpbody" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) // stringTools — транслитерация имён файлов для HTTP-заголовков. var stringTools = string_tools.NewStringTools() type server struct { proto.UnsafeEveningDetectiveServerServer usersService *users_service.UsersService uiService *ui_service.UiService fileService *file_service.FileService scenarioService *scenarios_service.ScenarioService gameService *game_service.GameService screenRepo *screen_sessions_repo.ScreenSessionsRepo mapRepo *game_map_repo.Repo } func NewServer( usersService *users_service.UsersService, uiService *ui_service.UiService, fileService *file_service.FileService, scenarioService *scenarios_service.ScenarioService, gameService *game_service.GameService, screenRepo *screen_sessions_repo.ScreenSessionsRepo, mapRepo *game_map_repo.Repo, ) proto.EveningDetectiveServerServer { return &server{ usersService: usersService, uiService: uiService, fileService: fileService, scenarioService: scenarioService, gameService: gameService, screenRepo: screenRepo, mapRepo: mapRepo, } } func (s *server) CreateScreenSession(ctx context.Context, _ *proto.CreateScreenSessionReq) (*proto.CreateScreenSessionRsp, error) { token := uuid.New().String() id, err := s.screenRepo.Create(ctx, hashScreenToken(token)) if err != nil { return nil, status.Error(codes.Internal, "не удалось создать сессию экрана") } return &proto.CreateScreenSessionRsp{ScreenId: fmt.Sprint(id), AccessToken: token}, nil } func (s *server) GetScreenState(ctx context.Context, req *proto.GetScreenStateReq) (*proto.GetScreenStateRsp, error) { if req == nil || req.ScreenId == "" || req.AccessToken == "" { return nil, status.Error(codes.InvalidArgument, "некорректные данные экрана") } id, err := strconv.Atoi(req.ScreenId) if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } session, err := s.screenRepo.Get(ctx, id, hashScreenToken(req.AccessToken)) if errors.Is(err, screen_sessions_repo.ErrScreenNotFound) { return nil, status.Error(codes.Unauthenticated, "недействительная сессия экрана") } if err != nil { return nil, status.Error(codes.Internal, "не удалось получить состояние экрана") } state := screenState(session) s.enrichScreenState(ctx, session, state) return &proto.GetScreenStateRsp{State: state}, nil } func (s *server) GetGameScreens(ctx context.Context, req *proto.GetGameScreensReq) (*proto.GetGameScreensRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.GameId <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор игры") } if _, err := s.gameService.GetGame(ctx, int(req.GameId)); err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } sessions, err := s.screenRepo.ListByGame(ctx, int(req.GameId)) if err != nil { return nil, status.Error(codes.Internal, "не удалось получить экраны игры") } states := make([]*proto.ScreenState, 0, len(sessions)) for _, session := range sessions { states = append(states, screenState(session)) } return &proto.GetGameScreensRsp{Screens: states}, nil } func (s *server) BindScreen(ctx context.Context, req *proto.BindScreenReq) (*proto.BindScreenRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.ScreenId == "" || req.AccessToken == "" || req.GameId <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректные данные привязки") } id, err := strconv.Atoi(req.ScreenId) if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } if _, err = s.gameService.GetGame(ctx, int(req.GameId)); err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } hash := hashScreenToken(req.AccessToken) if err = s.screenRepo.Bind(ctx, id, int(req.GameId), hash); err != nil { if errors.Is(err, screen_sessions_repo.ErrScreenNotFound) { return nil, status.Error(codes.Unauthenticated, "недействительная сессия экрана") } return nil, status.Error(codes.Internal, "не удалось привязать экран") } session, err := s.screenRepo.Get(ctx, id, hash) if err != nil { return nil, status.Error(codes.Internal, "не удалось получить состояние экрана") } state := screenState(session) s.enrichScreenState(ctx, session, state) return &proto.BindScreenRsp{State: state}, nil } func (s *server) UnbindScreen(ctx context.Context, req *proto.UnbindScreenReq) (*proto.UnbindScreenRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.ScreenId == "" { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } id, err := strconv.Atoi(req.ScreenId) if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } if err = s.screenRepo.Unbind(ctx, id); err != nil { if errors.Is(err, screen_sessions_repo.ErrScreenNotFound) { return nil, status.Error(codes.NotFound, "экран не найден") } return nil, status.Error(codes.Internal, "не удалось отвязать экран") } return &proto.UnbindScreenRsp{}, nil } func (s *server) SetScreenSound(ctx context.Context, req *proto.SetScreenSoundReq) (*proto.SetScreenSoundRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.ScreenId == "" { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } id, err := strconv.Atoi(req.ScreenId) if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } if err = s.screenRepo.SetSound(ctx, id, req.SoundEnabled); err != nil { if errors.Is(err, screen_sessions_repo.ErrScreenNotFound) { return nil, status.Error(codes.NotFound, "экран не найден") } return nil, status.Error(codes.Internal, "не удалось назначить экран со звуком") } return &proto.SetScreenSoundRsp{}, nil } func (s *server) GetGameMap(ctx context.Context, req *proto.GetGameMapReq) (*proto.GetGameMapRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.GameId <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор игры") } game, err := s.gameService.GetFullGame(ctx, int(req.GameId)) if err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } for _, team := range game.Teams { if err := s.mapRepo.Ensure(ctx, int(req.GameId), "team", team.ID); err != nil { return nil, status.Error(codes.Internal, "не удалось сохранить карту") } } screens, err := s.screenRepo.ListByGame(ctx, int(req.GameId)) if err != nil { return nil, status.Error(codes.Internal, "не удалось получить экраны") } for _, screen := range screens { if err := s.mapRepo.Ensure(ctx, int(req.GameId), "screen", screen.ID); err != nil { return nil, status.Error(codes.Internal, "не удалось сохранить карту") } } items, err := s.mapRepo.List(ctx, int(req.GameId)) if err != nil { return nil, status.Error(codes.Internal, "не удалось получить карту") } teamNames := map[int]string{} for _, team := range game.Teams { teamNames[team.ID] = team.Name } screenStates := map[int]*proto.ScreenState{} for _, screen := range screens { screenStates[screen.ID] = screenState(screen) } result := make([]*proto.MapItem, 0, len(items)) for _, item := range items { if item.Kind == "screen" && screenStates[item.ItemID] == nil { if err := s.mapRepo.Delete(ctx, item); err != nil { return nil, status.Error(codes.Internal, "не удалось обновить карту") } continue } mapped := &proto.MapItem{Kind: item.Kind, ItemId: int32(item.ItemID), X: int32(item.X), Y: int32(item.Y), Hidden: item.Hidden} if item.Kind == "team" { mapped.Label = teamNames[item.ItemID] } if state := screenStates[item.ItemID]; item.Kind == "screen" && state != nil { mapped.Label = "Экран " + state.Id mapped.Status = state.Status mapped.SoundEnabled = state.SoundEnabled } result = append(result, mapped) } return &proto.GetGameMapRsp{Items: result}, nil } func (s *server) UpdateGameMapItem(ctx context.Context, req *proto.UpdateGameMapItemReq) (*proto.UpdateGameMapItemRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.GameId <= 0 || req.ItemId <= 0 || (req.Kind != "team" && req.Kind != "screen") { return nil, status.Error(codes.InvalidArgument, "некорректный объект карты") } if err := s.mapRepo.Update(ctx, game_map_repo.Item{GameID: int(req.GameId), Kind: req.Kind, ItemID: int(req.ItemId), X: int(req.X), Y: int(req.Y), Hidden: req.Hidden}); err != nil { if errors.Is(err, game_map_repo.ErrItemNotFound) { return nil, status.Error(codes.NotFound, "объект карты не найден") } return nil, status.Error(codes.Internal, "не удалось изменить карту") } return &proto.UpdateGameMapItemRsp{}, nil } func canManageScreens(ctx context.Context) bool { claims, ok := ctx.Value("claims").(*processor_jwt.JWTClaims) if !ok || claims == nil { return false } return roles.HasRole(claims, roles.Organizer) || roles.HasRole(claims, roles.Author) } func hashScreenToken(token string) string { sum := sha256.Sum256([]byte(token)) return base64.RawURLEncoding.EncodeToString(sum[:]) } func screenState(session *screen_sessions_repo.ScreenSession) *proto.ScreenState { state := &proto.ScreenState{ Id: fmt.Sprint(session.ID), Status: "disconnected", SoundEnabled: session.Sound, Mode: proto.ScreenMode(session.Mode), StateVersion: session.Version, PositionMs: session.Position, Command: proto.ScreenCommand(session.Command), Volume: session.Volume, } if time.Since(session.LastSeen) <= 5*time.Second { state.Status = "connected" } if session.GameID != nil { gameID := int32(*session.GameID) state.GameId = &gameID if state.Mode == proto.ScreenMode_SCREEN_MODE_UNSPECIFIED { state.Mode = proto.ScreenMode_SCREEN_MODE_COVER } } return state } func (s *server) enrichScreenState(ctx context.Context, session *screen_sessions_repo.ScreenSession, state *proto.ScreenState) { if session.GameID == nil { return } game, err := s.gameService.GetGameMedia(ctx, *session.GameID) if err != nil { return } state.GameName = game.Name if game.Scenario != nil && game.Scenario.Image != nil { state.CoverImage = *game.Scenario.Image } if game.Scenario != nil && game.Scenario.Story != nil { state.BackgroundImage = game.Scenario.Story.Background if game.Scenario.Story.Introduction != nil { state.IntroAudio = game.Scenario.Story.Introduction.Audio } if game.Scenario.Story.Conclusion != nil { state.ConclusionVideo = game.Scenario.Story.Conclusion.Video } } } func (s *server) SetScreenMode(ctx context.Context, req *proto.SetScreenModeReq) (*proto.SetScreenModeRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.GameId <= 0 || req.Mode == proto.ScreenMode_SCREEN_MODE_UNSPECIFIED { return nil, status.Error(codes.InvalidArgument, "некорректный режим экрана") } if _, err := s.gameService.GetGame(ctx, int(req.GameId)); err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } if err := s.screenRepo.SetMode(ctx, int(req.GameId), int32(req.Mode)); err != nil { return nil, status.Error(codes.Internal, "не удалось изменить режим экранов") } return &proto.SetScreenModeRsp{}, nil } func (s *server) SendScreenCommand(ctx context.Context, req *proto.SendScreenCommandReq) (*proto.SendScreenCommandRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.GameId <= 0 || req.Command == proto.ScreenCommand_SCREEN_COMMAND_UNSPECIFIED { return nil, status.Error(codes.InvalidArgument, "некорректная команда экрана") } if _, err := s.gameService.GetGame(ctx, int(req.GameId)); err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } if req.PositionMs < 0 { return nil, status.Error(codes.InvalidArgument, "некорректная позиция") } if req.Volume < 0 || req.Volume > 100 { return nil, status.Error(codes.InvalidArgument, "некорректная громкость") } if err := s.screenRepo.SetCommand(ctx, int(req.GameId), int32(req.Command), req.PositionMs, req.Volume); err != nil { return nil, status.Error(codes.Internal, "не удалось сохранить позицию экранов") } return &proto.SendScreenCommandRsp{}, nil } func (s *server) GetScreenResults(ctx context.Context, req *proto.GetScreenResultsReq) (*proto.GetScreenResultsRsp, error) { if req == nil || req.ScreenId == "" || req.AccessToken == "" { return nil, status.Error(codes.InvalidArgument, "некорректные данные экрана") } id, err := strconv.Atoi(req.ScreenId) if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } session, err := s.screenRepo.Get(ctx, id, hashScreenToken(req.AccessToken)) if errors.Is(err, screen_sessions_repo.ErrScreenNotFound) { return nil, status.Error(codes.Unauthenticated, "недействительная сессия экрана") } if err != nil || session.GameID == nil { return nil, status.Error(codes.FailedPrecondition, "экран не привязан к игре") } game, err := s.gameService.GetGame(ctx, *session.GameID) if err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } if game.Status != "finish" { return nil, status.Error(codes.FailedPrecondition, "результаты доступны после завершения игры") } results, err := s.gameService.GetGameResults(ctx, *session.GameID) if err != nil { return nil, status.Error(codes.Internal, "не удалось получить результаты") } return &proto.GetScreenResultsRsp{Questions: mapResultsQuestions(results.Questions), Teams: mapTeamResults(results.Teams)}, nil } func (s *server) Ping(_ context.Context, _ *proto.PingReq) (*proto.PingRsp, error) { return &proto.PingRsp{}, nil } func (s *server) Echo(_ context.Context, req *proto.EchoReq) (*proto.EchoRsp, error) { return &proto.EchoRsp{Text: req.Text}, nil } func (s *server) Signup(ctx context.Context, req *proto.SignupReq) (*proto.SignupRsp, error) { ip, userAgent := clientInfoFromContext(ctx) err := s.usersService.AddUser( ctx, req.Username, req.Email, req.AcceptTerms, req.AcceptPrivacy, ip, userAgent, ) if err != nil { return &proto.SignupRsp{ Error: err.Error(), }, nil } return &proto.SignupRsp{}, nil } func (s *server) DeleteAccount(ctx context.Context, req *proto.DeleteAccountReq) (*proto.DeleteAccountRsp, error) { claims, ok := ctx.Value("claims").(*processor_jwt.JWTClaims) if !ok { return nil, status.Errorf(codes.Unauthenticated, "claims is not provided") } // Пароль приходит HTTP-заголовком X-Password (пробрасывается матчером // в main.go), чтобы креденшел не попадал в URL и логи. Для прямых // gRPC-клиентов заголовок передаётся в metadata под тем же ключом. password := "" if md, ok := metadata.FromIncomingContext(ctx); ok { if values := md.Get("x-password"); len(values) > 0 { password = values[0] } } err := s.usersService.DeleteAccount(ctx, claims.UserID, password) if err != nil { return &proto.DeleteAccountRsp{ Error: err.Error(), }, nil } return &proto.DeleteAccountRsp{}, nil } // GetTerms возвращает текст Пользовательского соглашения (постоянный URL // /api/terms, на который ссылается форма регистрации при акцепте). func (s *server) GetTerms(_ context.Context, _ *proto.GetTermsReq) (*proto.GetTermsRsp, error) { text, err := docs.FS.ReadFile("TERMS.md") if err != nil { return &proto.GetTermsRsp{Error: "Не удалось получить документ"}, nil } return &proto.GetTermsRsp{Text: string(text)}, nil } // GetPrivacy возвращает текст Политики конфиденциальности (постоянный URL // /api/privacy). func (s *server) GetPrivacy(_ context.Context, _ *proto.GetPrivacyReq) (*proto.GetPrivacyRsp, error) { text, err := docs.FS.ReadFile("PRIVACY.md") if err != nil { return &proto.GetPrivacyRsp{Error: "Не удалось получить документ"}, nil } return &proto.GetPrivacyRsp{Text: string(text)}, nil } // GetConsent возвращает текст согласия на обработку персональных данных // (постоянный URL /api/consent). func (s *server) GetConsent(_ context.Context, _ *proto.GetConsentReq) (*proto.GetConsentRsp, error) { text, err := docs.FS.ReadFile("CONSENT.md") if err != nil { return &proto.GetConsentRsp{Error: "Не удалось получить документ"}, nil } return &proto.GetConsentRsp{Text: string(text)}, nil } // clientInfoFromContext извлекает IP-адрес и user-agent из gRPC-метаданных. // // grpc-gateway пробрасывает HTTP-заголовки в метаданные так // (runtime.annotateContext / runtime.DefaultHeaderMatcher): // - x-forwarded-for — всегда, при этом gateway дописывает реальный RemoteAddr // клиента последним элементом списка; // - постоянные HTTP-заголовки (включая User-Agent) — с префиксом // grpcgateway-; // - остальные заголовки по умолчанию не пробрасываются: X-Real-IP и // X-Password включаются явным матчером в main.go. // // Полученный IP валидируется netip.ParseAddr: при прямой экспозиции gateway // клиент может подделать эти заголовки, поэтому в БД попадает только // корректный адрес. func clientInfoFromContext(ctx context.Context) (ip string, userAgent string) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return "", "" } // X-Real-IP от доверенного прокси — приоритетен: в этом случае последним // элементом X-Forwarded-For является адрес прокси, а не клиента. if values := md.Get("x-real-ip"); len(values) > 0 { if v := strings.TrimSpace(values[0]); v != "" { if _, err := netip.ParseAddr(v); err == nil { ip = v } } } if ip == "" { if values := md.Get("x-forwarded-for"); len(values) > 0 { parts := strings.Split(values[0], ",") // Идём с конца списка: последний элемент — реальный RemoteAddr // клиента (первые элементы мог подделать сам клиент). for i := len(parts) - 1; i >= 0; i-- { v := strings.TrimSpace(parts[i]) if v == "" { continue } if _, err := netip.ParseAddr(v); err == nil { ip = v break } } } } if values := md.Get("grpcgateway-user-agent"); len(values) > 0 { userAgent = values[0] } else if values := md.Get("user-agent"); len(values) > 0 { userAgent = values[0] } return ip, userAgent } func (s *server) RefreshPassword(ctx context.Context, req *proto.RefreshPasswordReq) (*proto.RefreshPasswordRsp, error) { err := s.usersService.RefreshPassword(ctx, req.Email) if err != nil { return &proto.RefreshPasswordRsp{ Error: err.Error(), }, nil } return &proto.RefreshPasswordRsp{}, nil } func (s *server) Login(ctx context.Context, req *proto.LoginReq) (*proto.LoginRsp, error) { accessToken, refreshToken, err := s.usersService.Login(ctx, req.Email, req.Password) if err != nil { return &proto.LoginRsp{ Error: err.Error(), }, nil } return &proto.LoginRsp{ AccessToken: accessToken, RefreshToken: refreshToken, }, nil } func (s *server) Refresh(ctx context.Context, req *proto.RefreshReq) (*proto.RefreshRsp, error) { accessToken, refreshToken, err := s.usersService.Refresh(ctx, req.RefreshToken) if err != nil { return &proto.RefreshRsp{ Error: err.Error(), }, nil } return &proto.RefreshRsp{ AccessToken: accessToken, RefreshToken: refreshToken, }, nil } func (s *server) GetUsers(ctx context.Context, req *proto.GetUsersReq) (*proto.GetUsersRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Admin) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } users, err := s.usersService.GetUsers(ctx) if err != nil { return &proto.GetUsersRsp{ Error: err.Error(), }, nil } resUsers := make([]*proto.User, 0, len(users)) for _, user := range users { resUsers = append(resUsers, mapUser(user)) } return &proto.GetUsersRsp{ Users: resUsers, }, nil } func (s *server) GetUserById(ctx context.Context, req *proto.GetUserByIdReq) (*proto.GetUserByIdRsp, error) { user, err := s.usersService.GetUserByID(ctx, int(req.Id)) if err != nil { return &proto.GetUserByIdRsp{ Error: err.Error(), }, nil } return &proto.GetUserByIdRsp{ User: mapUser(user), }, nil } func (s *server) GetMe(ctx context.Context, req *proto.GetMeReq) (*proto.GetMeRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) user, err := s.usersService.GetUserByID(ctx, claims.UserID) if err != nil { return &proto.GetMeRsp{ Error: err.Error(), }, nil } return &proto.GetMeRsp{ User: mapUser(user), }, nil } func (s *server) AddUserRole(ctx context.Context, req *proto.AddUserRoleReq) (*proto.AddUserRoleRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Admin) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } if err := s.usersService.AddUserRole(ctx, int(req.Id), req.Role); err != nil { return &proto.AddUserRoleRsp{ Error: err.Error(), }, nil } return &proto.AddUserRoleRsp{}, nil } func (s *server) DeleteUserRole(ctx context.Context, req *proto.DeleteUserRoleReq) (*proto.DeleteUserRoleRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Admin) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } if err := s.usersService.DeleteUserRole(ctx, int(req.Id), req.Role); err != nil { return &proto.DeleteUserRoleRsp{ Error: err.Error(), }, nil } return &proto.DeleteUserRoleRsp{}, nil } func (s *server) GetPermissions(ctx context.Context, req *proto.GetPermissionsReq) (*proto.GetPermissionsRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) permissions := s.uiService.GetPermissions(claims.Roles) return &proto.GetPermissionsRsp{ Permissions: permissions, }, nil } func (s *server) UploadFile(ctx context.Context, req *proto.UploadFileReq) (*proto.UploadFileRsp, error) { // Файлы (в т.ч. улики) загружают авторы сценариев в редакторе. claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } filename, fileType, err := s.fileService.UploadFile(ctx, req.Filename, req.Data) if err != nil { switch { case errors.Is(err, file_service.ErrFileTooLarge): return nil, status.Errorf(codes.ResourceExhausted, "%v", err) case errors.Is(err, file_service.ErrEmptyName), errors.Is(err, file_service.ErrEmptyData), errors.Is(err, file_service.ErrInvalidExtension), errors.Is(err, file_service.ErrInvalidContentType): return nil, status.Errorf(codes.InvalidArgument, "%v", err) default: return nil, status.Errorf(codes.Internal, "%v", err) } } return &proto.UploadFileRsp{ Filename: filename, FileType: fileType, }, nil } func (s *server) DownloadFile(ctx context.Context, req *proto.DownloadFileReq) (*httpbody.HttpBody, error) { file, err := s.fileService.DownloadFile(ctx, req.Filename) if err != nil { switch { case errors.Is(err, file_storage.ErrFileNotFound): return nil, status.Errorf(codes.NotFound, "%v", err) default: return nil, status.Errorf(codes.Internal, "%v", err) } } // nosniff — при публичном скачивании браузер не должен угадывать // Content-Type по содержимому (защита от stored-XSS при расхождении). if err := grpc.SetHeader(ctx, metadata.Pairs("X-Content-Type-Options", "nosniff")); err != nil { return nil, status.Errorf(codes.Internal, "failed to set response header: %v", err) } return &httpbody.HttpBody{ Data: file.Data, ContentType: file.Mime, }, nil } func (s *server) AddScenario(ctx context.Context, req *proto.AddScenarioReq) (*proto.AddScenarioRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } id, err := s.scenarioService.AddScenario(ctx, req.Name, claims.UserID) if err != nil { return &proto.AddScenarioRsp{ Error: err.Error(), }, nil } return &proto.AddScenarioRsp{ Id: int32(id), }, nil } func (s *server) GetMyScenarios(ctx context.Context, req *proto.GetMyScenariosReq) (*proto.GetMyScenariosRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } scenarios, err := s.scenarioService.GetScenariosByAuthorID(ctx, claims.UserID) if err != nil { return &proto.GetMyScenariosRsp{ Error: err.Error(), }, nil } return &proto.GetMyScenariosRsp{ Scenarios: mapScenarios(scenarios), }, nil } func (s *server) GetScenariosCatalog(ctx context.Context, req *proto.GetScenariosCatalogReq) (*proto.GetScenariosCatalogRsp, error) { scenarios, err := s.scenarioService.GetScenariosCatalog(ctx) if err != nil { return &proto.GetScenariosCatalogRsp{ Error: err.Error(), }, nil } return &proto.GetScenariosCatalogRsp{ Scenarios: mapScenarios(scenarios), }, nil } func (s *server) GetFullScenario(ctx context.Context, req *proto.GetScenarioReq) (*proto.GetScenarioRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } scenario, err := s.scenarioService.GetFullScenarioByID(ctx, int(req.Id)) if err != nil { return &proto.GetScenarioRsp{ Error: err.Error(), }, nil } return &proto.GetScenarioRsp{ Scenario: mapScenario(scenario), }, nil } func (s *server) GetScenario(ctx context.Context, req *proto.GetScenarioReq) (*proto.GetScenarioRsp, error) { scenario, err := s.scenarioService.GetScenarioByID(ctx, int(req.Id)) if err != nil { return &proto.GetScenarioRsp{ Error: err.Error(), }, nil } return &proto.GetScenarioRsp{ Scenario: mapScenario(scenario), }, nil } func (s *server) UpdateScenario(ctx context.Context, req *proto.UpdateScenarioReq) (*proto.UpdateScenarioRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.scenarioService.UpdateScenarioByID( ctx, int(req.Id), req.Name, req.Description, req.Image, claims.UserID, roles.HasRole(claims, roles.Admin), ) if err != nil { return &proto.UpdateScenarioRsp{ Error: err.Error(), }, nil } return &proto.UpdateScenarioRsp{}, nil } func (s *server) PublicScenario(ctx context.Context, req *proto.PublicScenarioReq) (*proto.PublicScenarioRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.scenarioService.UpdateScenarioStatusByID( ctx, int(req.Id), "public", claims.UserID, roles.HasRole(claims, roles.Admin), // публикация = момент отчуждения прав ) if err != nil { return &proto.PublicScenarioRsp{ Error: err.Error(), }, nil } return &proto.PublicScenarioRsp{}, nil } func (s *server) DraftScenario(ctx context.Context, req *proto.DraftScenarioReq) (*proto.DraftScenarioRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) // Снять с публикации опубликованный сценарий может только админ: // исключительное право отчуждено Оператору в момент публикации // (п. 7.2 Пользовательского соглашения, ст. 1234, 1269 ГК РФ). if !roles.HasRole(claims, roles.Admin) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.scenarioService.UpdateScenarioStatusByID( ctx, int(req.Id), "draft", claims.UserID, true, ) if err != nil { return &proto.DraftScenarioRsp{ Error: err.Error(), }, nil } return &proto.DraftScenarioRsp{}, nil } func (s *server) DeleteScenario(ctx context.Context, req *proto.DeleteScenarioReq) (*proto.DeleteScenarioRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } // Автор не может удалить опубликованный сценарий (исключительное право // отчуждено); админ может. err := s.scenarioService.DeleteScenarioByID( ctx, int(req.Id), claims.UserID, roles.HasRole(claims, roles.Admin), ) if err != nil { return &proto.DeleteScenarioRsp{ Error: err.Error(), }, nil } return &proto.DeleteScenarioRsp{}, nil } func (s *server) AddScenarioPlace(ctx context.Context, req *proto.AddScenarioPlaceReq) (*proto.AddScenarioPlaceRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.scenarioService.AddScenarioPlace( ctx, int(req.Id), convertPlace(req.Place), claims.UserID, roles.HasRole(claims, roles.Admin), ) if err != nil { return &proto.AddScenarioPlaceRsp{ Error: err.Error(), }, nil } return &proto.AddScenarioPlaceRsp{}, nil } func (s *server) UpdateScenarioPlace(ctx context.Context, req *proto.UpdateScenarioPlaceReq) (*proto.UpdateScenarioPlaceRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.scenarioService.UpdateScenarioPlace( ctx, int(req.Id), req.Code, convertPlace(req.Place), claims.UserID, roles.HasRole(claims, roles.Admin), ) if err != nil { return &proto.UpdateScenarioPlaceRsp{ Error: err.Error(), }, nil } return &proto.UpdateScenarioPlaceRsp{}, nil } func (s *server) DeleteScenarioPlace(ctx context.Context, req *proto.DeleteScenarioPlaceReq) (*proto.DeleteScenarioPlaceRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.scenarioService.DeleteScenarioPlace( ctx, int(req.Id), req.Code, claims.UserID, roles.HasRole(claims, roles.Admin), ) if err != nil { return &proto.DeleteScenarioPlaceRsp{ Error: err.Error(), }, nil } return &proto.DeleteScenarioPlaceRsp{}, nil } func (s *server) AddScenarioQuestion(ctx context.Context, req *proto.AddScenarioQuestionReq) (*proto.AddScenarioQuestionRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.scenarioService.AddScenarioQuestion( ctx, int(req.Id), convertQuestion(req.Question), claims.UserID, roles.HasRole(claims, roles.Admin), ) if err != nil { return &proto.AddScenarioQuestionRsp{ Error: err.Error(), }, nil } return &proto.AddScenarioQuestionRsp{}, nil } func (s *server) UpdateScenarioQuestion(ctx context.Context, req *proto.UpdateScenarioQuestionReq) (*proto.UpdateScenarioQuestionRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.scenarioService.UpdateScenarioQuestion( ctx, int(req.Id), req.Code, convertQuestion(req.Question), claims.UserID, roles.HasRole(claims, roles.Admin), ) if err != nil { return &proto.UpdateScenarioQuestionRsp{ Error: err.Error(), }, nil } return &proto.UpdateScenarioQuestionRsp{}, nil } func (s *server) DeleteScenarioQuestion(ctx context.Context, req *proto.DeleteScenarioQuestionReq) (*proto.DeleteScenarioQuestionRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.scenarioService.DeleteScenarioQuestion( ctx, int(req.Id), req.Code, claims.UserID, roles.HasRole(claims, roles.Admin), ) if err != nil { return &proto.DeleteScenarioQuestionRsp{ Error: err.Error(), }, nil } return &proto.DeleteScenarioQuestionRsp{}, nil } func (s *server) UpdateScenarioBackground(ctx context.Context, req *proto.UpdateScenarioBackgroundReq) (*proto.UpdateScenarioBackgroundRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.scenarioService.UpdateScenarioBackground( ctx, int(req.Id), req.Background, claims.UserID, roles.HasRole(claims, roles.Admin), ) if err != nil { return &proto.UpdateScenarioBackgroundRsp{ Error: err.Error(), }, nil } return &proto.UpdateScenarioBackgroundRsp{}, nil } func (s *server) UpdateScenarioIntro(ctx context.Context, req *proto.UpdateScenarioIntroReq) (*proto.UpdateScenarioIntroRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.scenarioService.UpdateScenarioIntro( ctx, int(req.Id), convertIntroduction(req.Introduction), claims.UserID, roles.HasRole(claims, roles.Admin), ) if err != nil { return &proto.UpdateScenarioIntroRsp{ Error: err.Error(), }, nil } return &proto.UpdateScenarioIntroRsp{}, nil } func (s *server) UpdateScenarioConclusion(ctx context.Context, req *proto.UpdateScenarioConclusionReq) (*proto.UpdateScenarioConclusionRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } if err := s.scenarioService.UpdateScenarioConclusion(ctx, int(req.Id), convertConclusion(req.Conclusion), claims.UserID, roles.HasRole(claims, roles.Admin)); err != nil { return &proto.UpdateScenarioConclusionRsp{Error: err.Error()}, nil } return &proto.UpdateScenarioConclusionRsp{}, nil } // DownloadScenarioArchive отдаёт ZIP-архив сценария; ошибки — gRPC-статусами. func (s *server) DownloadScenarioArchive(ctx context.Context, req *proto.DownloadScenarioArchiveReq) (*httpbody.HttpBody, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } data, name, err := s.scenarioService.DownloadArchive( ctx, int(req.Id), claims.UserID, roles.HasRole(claims, roles.Admin), ) if err != nil { switch { case errors.Is(err, scenarios_repo.ErrScenarioNotFound): return nil, status.Errorf(codes.NotFound, "%v", err) case errors.Is(err, scenarios_service.ErrScenarioNotOwner): return nil, status.Errorf(codes.PermissionDenied, "%v", err) default: return nil, status.Errorf(codes.Internal, "%v", err) } } // Имя файла транслитерируется (заголовки HTTP — только ASCII) и // санитайзится от header injection; заголовок пробрасывается gateway. transliterated := sanitizeFilename(stringTools.Transliterate(name)) if transliterated == "" { transliterated = "scenario" } filename := transliterated + ".zip" if err := grpc.SetHeader(ctx, metadata.Pairs("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))); err != nil { return nil, status.Errorf(codes.Internal, "failed to set response header: %v", err) } return &httpbody.HttpBody{ Data: data, ContentType: "application/zip", }, nil } // UploadScenarioArchive создаёт сценарий из ZIP-архива. Тело — сырые байты; // JSON base64 тоже поддерживается. func (s *server) UploadScenarioArchive(ctx context.Context, req *httpbody.HttpBody) (*proto.UploadScenarioArchiveRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } if req == nil || len(req.Data) == 0 { return nil, status.Errorf(codes.InvalidArgument, "пустое тело запроса") } if len(req.Data) > scenario_archive.MaxArchiveSize() { return nil, status.Errorf(codes.InvalidArgument, "архив больше максимального размера (%d байт)", scenario_archive.MaxArchiveSize()) } id, err := s.scenarioService.UploadArchive(ctx, req.Data, claims.UserID) if err != nil { return &proto.UploadScenarioArchiveRsp{ Error: err.Error(), }, nil } return &proto.UploadScenarioArchiveRsp{ Id: int32(id), }, nil } // sanitizeFilename — защита от инъекции заголовков в Content-Disposition. func sanitizeFilename(name string) string { replacer := strings.NewReplacer( `"`, "_", "\r", "_", "\n", "_", ) return replacer.Replace(name) } func (s *server) AddGame(ctx context.Context, req *proto.AddGameReq) (*proto.AddGameRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } if req.StartAt == nil { return nil, status.Errorf(codes.InvalidArgument, "startAt must not be nil") } id, err := s.gameService.AddGame( ctx, req.Name, req.Description, req.StartAt.AsTime(), req.ScenarioId, ) if err != nil { return &proto.AddGameRsp{ Error: err.Error(), }, nil } return &proto.AddGameRsp{ Id: int32(id), }, nil } func (s *server) DeleteGame(ctx context.Context, req *proto.DeleteGameReq) (*proto.DeleteGameRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.gameService.DeleteGame( ctx, int(req.Id), ) if err != nil { return &proto.DeleteGameRsp{ Error: err.Error(), }, nil } return &proto.DeleteGameRsp{}, nil } func (s *server) GetGame(ctx context.Context, req *proto.GetGameReq) (*proto.GetGameRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } game, err := s.gameService.GetFullGame(ctx, int(req.Id)) if err != nil { return &proto.GetGameRsp{ Error: err.Error(), }, nil } return &proto.GetGameRsp{ Game: mapGame(game), }, nil } func (s *server) GetGameAnswers(ctx context.Context, req *proto.GetGameAnswersReq) (*proto.GetGameAnswersRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } gameAnswers, err := s.gameService.GetGameAnswers(ctx, int(req.GameId)) if err != nil { return &proto.GetGameAnswersRsp{ Error: err.Error(), }, nil } return &proto.GetGameAnswersRsp{ Questions: mapQuestions(gameAnswers.Questions), Teams: mapTeamAnswers(gameAnswers.Teams), }, nil } func (s *server) GetGameResults(ctx context.Context, req *proto.GetGameResultsReq) (*proto.GetGameResultsRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } results, err := s.gameService.GetGameResults(ctx, int(req.GameId)) if err != nil { return &proto.GetGameResultsRsp{ Error: err.Error(), }, nil } return &proto.GetGameResultsRsp{ Questions: mapResultsQuestions(results.Questions), Teams: mapTeamResults(results.Teams), }, nil } func (s *server) GetGames(ctx context.Context, req *proto.GetGamesReq) (*proto.GetGamesRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } games, err := s.gameService.GetGames(ctx) if err != nil { return &proto.GetGamesRsp{ Error: err.Error(), }, nil } return &proto.GetGamesRsp{ Games: mapGames(games), }, nil } func (s *server) UpdateGame(ctx context.Context, req *proto.UpdateGameReq) (*proto.UpdateGameRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } if req.StartAt == nil { return nil, status.Errorf(codes.InvalidArgument, "startAt must not be nil") } err := s.gameService.UpdateGame( ctx, int(req.Id), req.Name, req.Description, req.StartAt.AsTime(), req.ScenarioId, ) if err != nil { return &proto.UpdateGameRsp{ Error: err.Error(), }, nil } return &proto.UpdateGameRsp{}, nil } func (s *server) StartGame(ctx context.Context, req *proto.StartGameReq) (*proto.StartGameRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.gameService.StartGame(ctx, int(req.Id)) if err != nil { return &proto.StartGameRsp{ Error: err.Error(), }, nil } return &proto.StartGameRsp{}, nil } func (s *server) PauseGame(ctx context.Context, req *proto.PauseGameReq) (*proto.PauseGameRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.gameService.PauseGame(ctx, int(req.Id)) if err != nil { return &proto.PauseGameRsp{ Error: err.Error(), }, nil } return &proto.PauseGameRsp{}, nil } func (s *server) PlayGame(ctx context.Context, req *proto.PlayGameReq) (*proto.PlayGameRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.gameService.PlayGame(ctx, int(req.Id)) if err != nil { return &proto.PlayGameRsp{ Error: err.Error(), }, nil } return &proto.PlayGameRsp{}, nil } func (s *server) FinishGame(ctx context.Context, req *proto.FinishGameReq) (*proto.FinishGameRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.gameService.FinishGame(ctx, int(req.Id)) if err != nil { return &proto.FinishGameRsp{ Error: err.Error(), }, nil } return &proto.FinishGameRsp{}, nil } func (s *server) ResetGame(ctx context.Context, req *proto.ResetGameReq) (*proto.ResetGameRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.gameService.ResetGame(ctx, int(req.Id)) if err != nil { return &proto.ResetGameRsp{ Error: err.Error(), }, nil } return &proto.ResetGameRsp{}, nil } func (s *server) AddTeam(ctx context.Context, req *proto.AddTeamReq) (*proto.AddTeamRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } team, err := s.gameService.AddTeam( ctx, claims.UserID, int(req.GameId), req.Name, ) if err != nil { return &proto.AddTeamRsp{ Error: err.Error(), }, nil } return &proto.AddTeamRsp{ Id: int32(team.ID), Password: team.Password, }, nil } func (s *server) UpdateTeam(ctx context.Context, req *proto.UpdateTeamReq) (*proto.UpdateTeamRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.gameService.UpdateTeam( ctx, int(req.Id), req.Name, ) if err != nil { return &proto.UpdateTeamRsp{ Error: err.Error(), }, nil } return &proto.UpdateTeamRsp{}, nil } func (s *server) DeleteTeam(ctx context.Context, req *proto.DeleteTeamReq) (*proto.DeleteTeamRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.gameService.DeleteTeam( ctx, int(req.Id), ) if err != nil { return &proto.DeleteTeamRsp{ Error: err.Error(), }, nil } return &proto.DeleteTeamRsp{}, nil } func (s *server) GiveTeamApplications(ctx context.Context, req *proto.GiveTeamApplicationsReq) (*proto.GiveTeamApplicationsRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.gameService.GiveTeamApplications( ctx, int(req.Id), req.Name, ) if err != nil { return &proto.GiveTeamApplicationsRsp{ Error: err.Error(), }, nil } return &proto.GiveTeamApplicationsRsp{}, nil } func (s *server) GetTeamStory(ctx context.Context, req *proto.GetTeamStoryReq) (*proto.GetTeamStoryRsp, error) { story, game, actions, err := s.gameService.GetTeamActions( ctx, int(req.Id), req.Password, ) if err != nil { return &proto.GetTeamStoryRsp{ Error: err.Error(), }, nil } protoActions := make([]*proto.TeamAction, 0, len(actions)) for _, action := range actions { protoActions = append(protoActions, &proto.TeamAction{Code: action.Code, Mcp: action.ViaMCP}) } return &proto.GetTeamStoryRsp{ Story: mapStory(story), Game: mapGame(game), Actions: protoActions, }, nil } func (s *server) AddTeamAction(ctx context.Context, req *proto.AddTeamActionReq) (*proto.AddTeamActionRsp, error) { err := s.gameService.AddTeamAction( ctx, int(req.Id), req.Password, req.Code, false, ) if err != nil { return &proto.AddTeamActionRsp{ Error: err.Error(), }, nil } return &proto.AddTeamActionRsp{}, nil } func (s *server) DeleteLastTeamAction(ctx context.Context, req *proto.DeleteLastTeamActionReq) (*proto.DeleteLastTeamActionRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } err := s.gameService.DeleteLastTeamAction( ctx, int(req.Id), ) if err != nil { return &proto.DeleteLastTeamActionRsp{ Error: err.Error(), }, nil } return &proto.DeleteLastTeamActionRsp{}, nil } func (s *server) GetTeamAnswers(ctx context.Context, req *proto.GetTeamAnswersReq) (*proto.GetTeamAnswersRsp, error) { answers, err := s.gameService.GetTeamAnswers( ctx, int(req.Id), req.Password, ) if err != nil { return &proto.GetTeamAnswersRsp{ Error: err.Error(), }, nil } return &proto.GetTeamAnswersRsp{ Answers: mapAnswers(answers), }, nil } func (s *server) SubmitTeamAnswers(ctx context.Context, req *proto.SubmitTeamAnswersReq) (*proto.SubmitTeamAnswersRsp, error) { err := s.gameService.SubmitTeamAnswers( ctx, int(req.Id), req.Password, convertAnswers(req.Answers), ) if err != nil { return &proto.SubmitTeamAnswersRsp{ Error: err.Error(), }, nil } return &proto.SubmitTeamAnswersRsp{}, nil } func (s *server) SetTeamAnswerScore(ctx context.Context, req *proto.SetTeamAnswerScoreReq) (*proto.SetTeamAnswerScoreRsp, error) { claims := ctx.Value("claims").(*processor_jwt.JWTClaims) if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { return nil, status.Errorf(codes.PermissionDenied, "permission denied") } var score *int if req.Score != nil { v := int(req.GetScore()) score = &v } err := s.gameService.SetTeamAnswerScore( ctx, int(req.Id), req.QuestionCode, score, ) if err != nil { return &proto.SetTeamAnswerScoreRsp{ Error: err.Error(), }, nil } return &proto.SetTeamAnswerScoreRsp{}, nil } func (s *server) ChangeUserPassword(ctx context.Context, req *proto.ChangeUserPasswordReq) (*proto.ChangeUserPasswordRsp, error) { claims, ok := ctx.Value("claims").(*processor_jwt.JWTClaims) if !ok || claims == nil { return nil, status.Error(codes.Unauthenticated, "Требуется авторизация") } if !roles.HasRole(claims, roles.Admin) { return nil, status.Error(codes.PermissionDenied, "Недостаточно прав") } err := s.usersService.ChangeUserPassword(ctx, claims.UserID, int(req.GetId()), req.GetNewPassword()) switch { case errors.Is(err, users_service.ErrPasswordChangeForbidden): return nil, status.Error(codes.PermissionDenied, err.Error()) case errors.Is(err, users_service.ErrInvalidPasswordChange): return nil, status.Error(codes.InvalidArgument, err.Error()) case errors.Is(err, users_repo.ErrUserNotFound): return nil, status.Error(codes.NotFound, err.Error()) case err != nil: return nil, status.Error(codes.Internal, "Не удалось изменить пароль") } return &proto.ChangeUserPasswordRsp{}, nil }