generated from VLADIMIR/template
up
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"evening_detective_server/internal/modules/processor_jwt"
|
||||
"evening_detective_server/internal/modules/roles"
|
||||
"evening_detective_server/internal/services/device_presence_service"
|
||||
proto "evening_detective_server/proto"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func (s *server) DeviceHeartbeat(ctx context.Context, req *proto.DeviceHeartbeatReq) (*proto.DeviceHeartbeatRsp, error) {
|
||||
if req == nil {
|
||||
req = &proto.DeviceHeartbeatReq{}
|
||||
}
|
||||
ip, userAgent := clientInfoFromContext(ctx)
|
||||
claims, authenticated := processor_jwt.ClaimsFromContext(ctx)
|
||||
input := device_presence_service.Heartbeat{
|
||||
DeviceID: req.DeviceID,
|
||||
ConnectionID: req.ConnectionID,
|
||||
OrganizerGameID: deviceOptionalID(req.OrganizerGameID),
|
||||
TeamID: deviceOptionalID(req.TeamID),
|
||||
Password: req.Password,
|
||||
ScreenID: req.ScreenID,
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
IsAuthenticated: authenticated,
|
||||
CanManage: authenticated && (roles.HasRole(claims, roles.Organizer) || roles.HasRole(claims, roles.Author)),
|
||||
}
|
||||
if req.AccessToken != "" {
|
||||
input.ScreenTokenHash = hashScreenToken(req.AccessToken)
|
||||
}
|
||||
enabled, now, err := s.devicePresenceService.Heartbeat(ctx, input)
|
||||
if err != nil {
|
||||
return nil, deviceErrorStatus(err)
|
||||
}
|
||||
return &proto.DeviceHeartbeatRsp{
|
||||
Enabled: &enabled,
|
||||
ServerTime: timestamppb.New(now),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *server) GetDevices(ctx context.Context, req *proto.GetDevicesReq) (*proto.GetDevicesRsp, error) {
|
||||
if req == nil {
|
||||
req = &proto.GetDevicesReq{}
|
||||
}
|
||||
claims, authenticated := processor_jwt.ClaimsFromContext(ctx)
|
||||
canRead := authenticated && (roles.HasRole(claims, roles.Organizer) || roles.HasRole(claims, roles.Developer))
|
||||
devices, now, err := s.devicePresenceService.List(ctx, deviceOptionalID(req.GameID), canRead)
|
||||
if err != nil {
|
||||
return nil, deviceErrorStatus(err)
|
||||
}
|
||||
return &proto.GetDevicesRsp{
|
||||
Devices: mapVenueDevices(devices),
|
||||
ServerTime: timestamppb.New(now),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
"evening_detective_server/internal/services/device_presence_service"
|
||||
"evening_detective_server/internal/services/game_service"
|
||||
"evening_detective_server/internal/services/screen_service"
|
||||
"evening_detective_server/internal/services/team_service"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func deviceErrorStatus(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, context.Canceled):
|
||||
return status.Error(codes.Canceled, "запрос отменён")
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return status.Error(codes.DeadlineExceeded, "время запроса истекло")
|
||||
case errors.Is(err, device_presence_service.ErrInvalid):
|
||||
return status.Error(codes.InvalidArgument, device_presence_service.ErrInvalid.Error())
|
||||
case errors.Is(err, device_presence_service.ErrDisabled):
|
||||
return status.Error(codes.FailedPrecondition, device_presence_service.ErrDisabled.Error())
|
||||
case errors.Is(err, device_presence_service.ErrPermission):
|
||||
return status.Error(codes.PermissionDenied, device_presence_service.ErrPermission.Error())
|
||||
case errors.Is(err, device_presence_service.ErrUnauthorized):
|
||||
return status.Error(codes.Unauthenticated, device_presence_service.ErrUnauthorized.Error())
|
||||
case errors.Is(err, device_presence_service.ErrFull):
|
||||
return status.Error(codes.ResourceExhausted, device_presence_service.ErrFull.Error())
|
||||
case errors.Is(err, team_service.ErrTeamNotFound):
|
||||
return status.Error(codes.NotFound, team_service.ErrTeamNotFound.Error())
|
||||
case errors.Is(err, game_service.ErrGameNotFound):
|
||||
return status.Error(codes.NotFound, game_service.ErrGameNotFound.Error())
|
||||
case errors.Is(err, screen_service.ErrScreenNotFound):
|
||||
return status.Error(codes.Unauthenticated, "недействительная сессия экрана")
|
||||
default:
|
||||
log.Printf("device presence: %v", err)
|
||||
return status.Error(codes.Internal, "не удалось обновить учёт устройств")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"evening_detective_server/internal/services/device_presence_service"
|
||||
proto "evening_detective_server/proto"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func deviceOptionalID(id *int32) *int {
|
||||
if id == nil {
|
||||
return nil
|
||||
}
|
||||
value := int(*id)
|
||||
return &value
|
||||
}
|
||||
|
||||
func devicePublicID(id int) *int32 {
|
||||
if id == 0 {
|
||||
return nil
|
||||
}
|
||||
value := int32(id)
|
||||
return &value
|
||||
}
|
||||
|
||||
func mapVenueDevices(devices []device_presence_service.Device) []*proto.VenueDevice {
|
||||
result := make([]*proto.VenueDevice, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
connections := make([]*proto.DeviceConnection, 0, len(device.Connections))
|
||||
for _, connection := range device.Connections {
|
||||
connections = append(connections, &proto.DeviceConnection{
|
||||
ID: connection.ID,
|
||||
FirstSeenAt: timestamppb.New(connection.FirstSeenAt),
|
||||
LastSeenAt: timestamppb.New(connection.LastSeenAt),
|
||||
IP: connection.IP,
|
||||
UserAgent: connection.UserAgent,
|
||||
Connected: connection.Connected,
|
||||
GameID: devicePublicID(connection.GameID),
|
||||
TeamID: devicePublicID(connection.TeamID),
|
||||
ScreenID: devicePublicID(connection.ScreenID),
|
||||
Kind: connection.Kind,
|
||||
})
|
||||
}
|
||||
result = append(result, &proto.VenueDevice{
|
||||
ID: device.ID,
|
||||
FirstSeenAt: timestamppb.New(device.FirstSeenAt),
|
||||
LastSeenAt: timestamppb.New(device.LastSeenAt),
|
||||
Connected: device.Connected,
|
||||
Connections: connections,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"evening_detective_server/internal/modules/processor_jwt"
|
||||
"evening_detective_server/internal/services/device_presence_service"
|
||||
"evening_detective_server/internal/services/game_service"
|
||||
"evening_detective_server/internal/services/screen_service"
|
||||
"evening_detective_server/internal/services/team_service"
|
||||
proto "evening_detective_server/proto"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
func TestDeviceTransport(t *testing.T) {
|
||||
s := &server{devicePresenceService: device_presence_service.NewDevicePresenceService(true, nil, nil, nil)}
|
||||
ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("x-real-ip", "192.0.2.3", "grpcgateway-user-agent", "test-browser"))
|
||||
req := &proto.DeviceHeartbeatReq{DeviceID: uuid.NewString(), ConnectionID: uuid.NewString()}
|
||||
rsp, err := s.DeviceHeartbeat(ctx, req)
|
||||
if err != nil || !rsp.GetEnabled() || rsp.ServerTime == nil {
|
||||
t.Fatal(rsp, err)
|
||||
}
|
||||
for _, role := range []string{"organizer", "admin", "developer", "author", "user", ""} {
|
||||
t.Run(role, func(t *testing.T) {
|
||||
roleCtx := processor_jwt.ContextWithClaims(ctx, &processor_jwt.JWTClaims{Roles: []string{role}})
|
||||
devices, err := s.GetDevices(roleCtx, &proto.GetDevicesReq{})
|
||||
if role == "author" || role == "user" || role == "" {
|
||||
if status.Code(err) != codes.PermissionDenied {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil || len(devices.Devices) != 1 {
|
||||
t.Fatal(devices, err)
|
||||
}
|
||||
connection := devices.Devices[0].Connections[0]
|
||||
if connection.IP != "192.0.2.3" || connection.UserAgent != "test-browser" {
|
||||
t.Fatal(connection)
|
||||
}
|
||||
raw, err := protojson.Marshal(devices)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, secret := range []string{"password", "accessToken", "ScreenTokenHash", "email", "URL"} {
|
||||
if strings.Contains(string(raw), secret) {
|
||||
t.Fatal("secret field in response")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
req.DeviceID = "bad"
|
||||
if _, err = s.DeviceHeartbeat(ctx, req); status.Code(err) != codes.InvalidArgument {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.devicePresenceService = device_presence_service.NewDevicePresenceService(false, nil, nil, nil)
|
||||
rsp, err = s.DeviceHeartbeat(ctx, req)
|
||||
if err != nil || rsp.GetEnabled() {
|
||||
t.Fatal(rsp, err)
|
||||
}
|
||||
raw, err := protojson.Marshal(rsp)
|
||||
if err != nil || !strings.Contains(string(raw), `"enabled":false`) {
|
||||
t.Fatal("disabled flag must be explicit", string(raw), err)
|
||||
}
|
||||
if _, err = s.GetDevices(ctx, &proto.GetDevicesReq{}); status.Code(err) != codes.FailedPrecondition {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceErrors(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{context.Canceled, codes.Canceled},
|
||||
{context.DeadlineExceeded, codes.DeadlineExceeded},
|
||||
{device_presence_service.ErrInvalid, codes.InvalidArgument},
|
||||
{device_presence_service.ErrDisabled, codes.FailedPrecondition},
|
||||
{device_presence_service.ErrPermission, codes.PermissionDenied},
|
||||
{device_presence_service.ErrUnauthorized, codes.Unauthenticated},
|
||||
{device_presence_service.ErrFull, codes.ResourceExhausted},
|
||||
{team_service.ErrTeamNotFound, codes.NotFound},
|
||||
{game_service.ErrGameNotFound, codes.NotFound},
|
||||
{screen_service.ErrScreenNotFound, codes.Unauthenticated},
|
||||
{errors.New("SQL password=secret /private/path"), codes.Internal},
|
||||
} {
|
||||
err := deviceErrorStatus(test.err)
|
||||
if status.Code(err) != test.code || strings.Contains(err.Error(), "secret") || strings.Contains(err.Error(), "/private") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"evening_detective_server/internal/services/answer_service"
|
||||
"evening_detective_server/internal/services/device_presence_service"
|
||||
"evening_detective_server/internal/services/file_service"
|
||||
"evening_detective_server/internal/services/game_export_service"
|
||||
"evening_detective_server/internal/services/game_import_service"
|
||||
@@ -26,6 +27,7 @@ import (
|
||||
type server struct {
|
||||
proto.UnsafeEveningDetectiveServerServer
|
||||
|
||||
devicePresenceService *device_presence_service.DevicePresenceService
|
||||
usersService *users_service.UsersService
|
||||
uiService *ui_service.UIService
|
||||
taskService *task_service.TaskService
|
||||
@@ -70,8 +72,10 @@ func NewServer(
|
||||
gameImportService *game_import_service.GameImportService,
|
||||
gameResultsService *game_results_package_service.GameResultsPackageService,
|
||||
gameResultsApplyService *game_results_package_service.GameResultsApplyService,
|
||||
devicePresenceService *device_presence_service.DevicePresenceService,
|
||||
) proto.EveningDetectiveServerServer {
|
||||
return &server{
|
||||
devicePresenceService: devicePresenceService,
|
||||
gameImportService: gameImportService,
|
||||
gameResultsService: gameResultsService,
|
||||
gameResultsApplyService: gameResultsApplyService,
|
||||
|
||||
Reference in New Issue
Block a user