diff --git a/bin/evening_detective_server b/bin/evening_detective_server index dc7ba83..5706bbf 100755 Binary files a/bin/evening_detective_server and b/bin/evening_detective_server differ diff --git a/cmd/evening_detective_server/main.go b/cmd/evening_detective_server/main.go index 82c773c..75b9be9 100644 --- a/cmd/evening_detective_server/main.go +++ b/cmd/evening_detective_server/main.go @@ -19,6 +19,7 @@ import ( "evening_detective_server/internal/repos/games_repo" "evening_detective_server/internal/repos/refresh_tokens_repo" "evening_detective_server/internal/repos/scenarios_repo" + "evening_detective_server/internal/repos/screen_sessions_repo" "evening_detective_server/internal/repos/team_answers_repo" "evening_detective_server/internal/repos/teams_repo" "evening_detective_server/internal/repos/users_repo" @@ -128,6 +129,7 @@ func main() { actionsRepo := actions_repo.NewActionsRepo(dbpool) applicationsRepo := applications_repo.NewApplicationsRepo(dbpool) answersRepo := team_answers_repo.NewTeamAnswersRepo(dbpool) + screenRepo := screen_sessions_repo.NewScreenSessionsRepo(dbpool) gameService := game_service.NewGameService( gameRepo, teamRepo, @@ -172,6 +174,8 @@ func main() { "/crabs.evening_detective_server.EveningDetectiveServer/GetTerms": true, "/crabs.evening_detective_server.EveningDetectiveServer/GetPrivacy": true, "/crabs.evening_detective_server.EveningDetectiveServer/GetConsent": true, + "/crabs.evening_detective_server.EveningDetectiveServer/CreateScreenSession": true, + "/crabs.evening_detective_server.EveningDetectiveServer/GetScreenState": true, }, processorJWT, ), @@ -186,6 +190,7 @@ func main() { fileService, scenarioService, gameService, + screenRepo, ), ) diff --git a/internal/app/server.go b/internal/app/server.go index 5dbdb8c..a188432 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -2,9 +2,12 @@ package app import ( "context" + "crypto/sha256" + "encoding/base64" "errors" "fmt" "net/netip" + "strconv" "strings" "evening_detective_server/docs" @@ -14,6 +17,7 @@ import ( "evening_detective_server/internal/modules/scenario_archive" "evening_detective_server/internal/modules/string_tools" "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" @@ -22,6 +26,7 @@ import ( "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" @@ -40,6 +45,7 @@ type server struct { fileService *file_service.FileService scenarioService *scenarios_service.ScenarioService gameService *game_service.GameService + screenRepo *screen_sessions_repo.ScreenSessionsRepo } func NewServer( @@ -48,6 +54,7 @@ func NewServer( fileService *file_service.FileService, scenarioService *scenarios_service.ScenarioService, gameService *game_service.GameService, + screenRepo *screen_sessions_repo.ScreenSessionsRepo, ) proto.EveningDetectiveServerServer { return &server{ usersService: usersService, @@ -55,23 +62,111 @@ func NewServer( fileService: fileService, scenarioService: scenarioService, gameService: gameService, + screenRepo: screenRepo, } } -func (s *server) CreateScreenSession(context.Context, *proto.CreateScreenSessionReq) (*proto.CreateScreenSessionRsp, error) { - panic("CreateScreenSession is not implemented") +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(context.Context, *proto.GetScreenStateReq) (*proto.GetScreenStateRsp, error) { - panic("GetScreenState is not implemented") +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, "не удалось получить состояние экрана") + } + return &proto.GetScreenStateRsp{State: screenState(session)}, nil } -func (s *server) BindScreen(context.Context, *proto.BindScreenReq) (*proto.BindScreenRsp, error) { - panic("BindScreen is not implemented") +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, "не удалось получить состояние экрана") + } + return &proto.BindScreenRsp{State: screenState(session)}, nil } -func (s *server) UnbindScreen(context.Context, *proto.UnbindScreenReq) (*proto.UnbindScreenRsp, error) { - panic("UnbindScreen is not implemented") +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 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: "waiting", + Mode: proto.ScreenMode_SCREEN_MODE_UNSPECIFIED, + } + if session.GameID != nil { + gameID := int32(*session.GameID) + state.GameId = &gameID + state.Status = "bound" + state.Mode = proto.ScreenMode_SCREEN_MODE_COVER + } + return state } func (s *server) SetScreenMode(context.Context, *proto.SetScreenModeReq) (*proto.SetScreenModeRsp, error) { diff --git a/internal/repos/screen_sessions_repo/repo.go b/internal/repos/screen_sessions_repo/repo.go new file mode 100644 index 0000000..7edc319 --- /dev/null +++ b/internal/repos/screen_sessions_repo/repo.go @@ -0,0 +1,77 @@ +package screen_sessions_repo + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +var ErrScreenNotFound = errors.New("Экран не найден") + +type ScreenSession struct { + ID int + GameID *int +} + +type ScreenSessionsRepo struct { + pool *pgxpool.Pool +} + +func NewScreenSessionsRepo(pool *pgxpool.Pool) *ScreenSessionsRepo { + return &ScreenSessionsRepo{pool: pool} +} + +func (r *ScreenSessionsRepo) Create(ctx context.Context, tokenHash string) (int, error) { + var id int + err := r.pool.QueryRow(ctx, ` + INSERT INTO screen_sessions (access_token_hash) + VALUES ($1) + RETURNING id`, tokenHash).Scan(&id) + return id, err +} + +func (r *ScreenSessionsRepo) Get(ctx context.Context, id int, tokenHash string) (*ScreenSession, error) { + session := &ScreenSession{ID: id} + err := r.pool.QueryRow(ctx, ` + SELECT game_id + FROM screen_sessions + WHERE id = $1 AND access_token_hash = $2`, id, tokenHash). + Scan(&session.GameID) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrScreenNotFound + } + if err != nil { + return nil, err + } + return session, nil +} + +func (r *ScreenSessionsRepo) Bind(ctx context.Context, id, gameID int, tokenHash string) error { + result, err := r.pool.Exec(ctx, ` + UPDATE screen_sessions + SET game_id = $1, updated_at = NOW() + WHERE id = $2 AND access_token_hash = $3`, gameID, id, tokenHash) + if err != nil { + return err + } + if result.RowsAffected() == 0 { + return ErrScreenNotFound + } + return nil +} + +func (r *ScreenSessionsRepo) Unbind(ctx context.Context, id int) error { + result, err := r.pool.Exec(ctx, ` + UPDATE screen_sessions + SET game_id = NULL, updated_at = NOW() + WHERE id = $1`, id) + if err != nil { + return err + } + if result.RowsAffected() == 0 { + return ErrScreenNotFound + } + return nil +} diff --git a/migrations/20260910120000_create_screen_sessions_table.sql b/migrations/20260910120000_create_screen_sessions_table.sql new file mode 100644 index 0000000..9835dfd --- /dev/null +++ b/migrations/20260910120000_create_screen_sessions_table.sql @@ -0,0 +1,11 @@ +-- +goose Up +CREATE TABLE IF NOT EXISTS screen_sessions ( + id BIGSERIAL PRIMARY KEY, + access_token_hash TEXT NOT NULL UNIQUE, + game_id INTEGER REFERENCES games (id) ON DELETE SET NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +-- +goose Down +DROP TABLE IF EXISTS screen_sessions;