generated from VLADIMIR/template
up
This commit is contained in:
@@ -98,6 +98,16 @@ make test
|
|||||||
восстановление worker и команды проверки описаны в
|
восстановление worker и команды проверки описаны в
|
||||||
[`docs/GAME_PACKAGE.md`](docs/GAME_PACKAGE.md#экспорт-со-статусом-офлайн-режима-этап-5).
|
[`docs/GAME_PACKAGE.md`](docs/GAME_PACKAGE.md#экспорт-со-статусом-офлайн-режима-этап-5).
|
||||||
|
|
||||||
|
## Учёт устройств площадки
|
||||||
|
|
||||||
|
В `APP_MODE=venue` публичный `POST /api/devices/heartbeat` учитывает подключения
|
||||||
|
браузеров в памяти; `GET /api/devices` доступен организатору, администратору и
|
||||||
|
разработчику, с необязательным `gameID`. В prod heartbeat возвращает
|
||||||
|
`enabled=false`, список недоступен. TTL связи — менее 60 секунд, удаление —
|
||||||
|
через 15 минут, предел — 1024 подключения. Credentials не сохраняются.
|
||||||
|
Подробности и ограничения описаны в
|
||||||
|
[workspace README](../README.md#учёт-устройств-этап-12).
|
||||||
|
|
||||||
## Документация и юридические документы
|
## Документация и юридические документы
|
||||||
|
|
||||||
- [`docs/TERMS.md`](docs/TERMS.md) — Пользовательское соглашение (оферта); раздаётся сервером по постоянному URL `/api/terms`;
|
- [`docs/TERMS.md`](docs/TERMS.md) — Пользовательское соглашение (оферта); раздаётся сервером по постоянному URL `/api/terms`;
|
||||||
|
|||||||
@@ -41,6 +41,23 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
service EveningDetectiveServer {
|
service EveningDetectiveServer {
|
||||||
|
rpc DeviceHeartbeat(DeviceHeartbeatReq) returns (DeviceHeartbeatRsp) {
|
||||||
|
option (google.api.http) = { post: "/api/devices/heartbeat" body: "*" };
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
tags : "Устройства";
|
||||||
|
summary : "Обновить присутствие подключения браузера";
|
||||||
|
description: "Публичный endpoint. Контексты взаимоисключающие и проверяются сервером. В prod enabled=false без регистрации. UUID обозначает браузерный профиль, не физический телефон.";
|
||||||
|
security : {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
rpc GetDevices(GetDevicesReq) returns (GetDevicesRsp) {
|
||||||
|
option (google.api.http) = { get: "/api/devices" };
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
tags : "Устройства";
|
||||||
|
summary : "Получить устройства площадки";
|
||||||
|
description: "Только venue; организатор, администратор или разработчик. Связь актуальна менее 60 секунд; подключения удаляются через 15 минут. Фильтр gameID оставляет только подключения указанной игры.";
|
||||||
|
};
|
||||||
|
}
|
||||||
rpc GetTasks(GetTasksReq) returns (GetTasksRsp) {
|
rpc GetTasks(GetTasksReq) returns (GetTasksRsp) {
|
||||||
option (google.api.http) = { get: "/api/tasks" };
|
option (google.api.http) = { get: "/api/tasks" };
|
||||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
@@ -2382,3 +2399,46 @@ message GameResultsPackageRsp {
|
|||||||
string packageID = 2;
|
string packageID = 2;
|
||||||
string appliedAt = 3;
|
string appliedAt = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message DeviceHeartbeatReq {
|
||||||
|
string deviceID = 1;
|
||||||
|
string connectionID = 2;
|
||||||
|
optional int32 organizerGameID = 3;
|
||||||
|
optional int32 teamID = 4;
|
||||||
|
string password = 5;
|
||||||
|
string screenID = 6;
|
||||||
|
string accessToken = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeviceHeartbeatRsp {
|
||||||
|
optional bool enabled = 1;
|
||||||
|
google.protobuf.Timestamp serverTime = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetDevicesReq { optional int32 gameID = 1; }
|
||||||
|
|
||||||
|
message GetDevicesRsp {
|
||||||
|
repeated VenueDevice devices = 1;
|
||||||
|
google.protobuf.Timestamp serverTime = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message VenueDevice {
|
||||||
|
string ID = 1;
|
||||||
|
google.protobuf.Timestamp firstSeenAt = 2;
|
||||||
|
google.protobuf.Timestamp lastSeenAt = 3;
|
||||||
|
bool connected = 4;
|
||||||
|
repeated DeviceConnection connections = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeviceConnection {
|
||||||
|
string ID = 1;
|
||||||
|
google.protobuf.Timestamp firstSeenAt = 2;
|
||||||
|
google.protobuf.Timestamp lastSeenAt = 3;
|
||||||
|
string IP = 4;
|
||||||
|
string userAgent = 5;
|
||||||
|
bool connected = 6;
|
||||||
|
optional int32 gameID = 7;
|
||||||
|
optional int32 teamID = 8;
|
||||||
|
optional int32 screenID = 9;
|
||||||
|
string kind = 10; // browser, organizer, team, screen
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -35,6 +35,7 @@ import (
|
|||||||
"evening_detective_server/internal/repos/tickets_repo"
|
"evening_detective_server/internal/repos/tickets_repo"
|
||||||
"evening_detective_server/internal/repos/users_repo"
|
"evening_detective_server/internal/repos/users_repo"
|
||||||
"evening_detective_server/internal/services/answer_service"
|
"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/file_service"
|
||||||
"evening_detective_server/internal/services/game_export_service"
|
"evening_detective_server/internal/services/game_export_service"
|
||||||
"evening_detective_server/internal/services/game_import_service"
|
"evening_detective_server/internal/services/game_import_service"
|
||||||
@@ -228,6 +229,8 @@ func main() {
|
|||||||
log.Fatalln("Failed to listen:", err)
|
log.Fatalln("Failed to listen:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
devicePresenceService := device_presence_service.NewDevicePresenceService(venueMode, gameService, teamService, screenService)
|
||||||
|
|
||||||
s := grpc.NewServer(
|
s := grpc.NewServer(
|
||||||
grpc.MaxRecvMsgSize(grpcMsgLimit),
|
grpc.MaxRecvMsgSize(grpcMsgLimit),
|
||||||
grpc.UnaryInterceptor(
|
grpc.UnaryInterceptor(
|
||||||
@@ -237,6 +240,7 @@ func main() {
|
|||||||
"/crabs.evening_detective_server.EveningDetectiveServer/GetParticipation": true,
|
"/crabs.evening_detective_server.EveningDetectiveServer/GetParticipation": true,
|
||||||
"/crabs.evening_detective_server.EveningDetectiveServer/GetTicket": true,
|
"/crabs.evening_detective_server.EveningDetectiveServer/GetTicket": true,
|
||||||
"/crabs.evening_detective_server.EveningDetectiveServer/AnswerParticipation": true,
|
"/crabs.evening_detective_server.EveningDetectiveServer/AnswerParticipation": true,
|
||||||
|
"/crabs.evening_detective_server.EveningDetectiveServer/DeviceHeartbeat": true,
|
||||||
"/crabs.evening_detective_server.EveningDetectiveServer/Ping": true,
|
"/crabs.evening_detective_server.EveningDetectiveServer/Ping": true,
|
||||||
"/crabs.evening_detective_server.EveningDetectiveServer/Echo": true,
|
"/crabs.evening_detective_server.EveningDetectiveServer/Echo": true,
|
||||||
"/crabs.evening_detective_server.EveningDetectiveServer/Signup": true,
|
"/crabs.evening_detective_server.EveningDetectiveServer/Signup": true,
|
||||||
@@ -287,6 +291,7 @@ func main() {
|
|||||||
gameImportService,
|
gameImportService,
|
||||||
gameResultsService,
|
gameResultsService,
|
||||||
gameResultsApplyService,
|
gameResultsApplyService,
|
||||||
|
devicePresenceService,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -333,6 +333,74 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/devices": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Получить устройства площадки",
|
||||||
|
"description": "Только venue; организатор, администратор или разработчик. Связь актуальна менее 60 секунд; подключения удаляются через 15 минут. Фильтр gameID оставляет только подключения указанной игры.",
|
||||||
|
"operationId": "EveningDetectiveServer_GetDevices",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "A successful response.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/evening_detective_serverGetDevicesRsp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "An unexpected error response.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/rpcStatus"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "gameID",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Устройства"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/devices/heartbeat": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Обновить присутствие подключения браузера",
|
||||||
|
"description": "Публичный endpoint. Контексты взаимоисключающие и проверяются сервером. В prod enabled=false без регистрации. UUID обозначает браузерный профиль, не физический телефон.",
|
||||||
|
"operationId": "EveningDetectiveServer_DeviceHeartbeat",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "A successful response.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/evening_detective_serverDeviceHeartbeatRsp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "An unexpected error response.",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/rpcStatus"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/evening_detective_serverDeviceHeartbeatReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Устройства"
|
||||||
|
],
|
||||||
|
"security": []
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/files/upload": {
|
"/api/files/upload": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Загрузить файл",
|
"summary": "Загрузить файл",
|
||||||
@@ -4700,6 +4768,87 @@
|
|||||||
},
|
},
|
||||||
"description": "Developer tasks use new, in_progress, review and done statuses."
|
"description": "Developer tasks use new, in_progress, review and done statuses."
|
||||||
},
|
},
|
||||||
|
"evening_detective_serverDeviceConnection": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ID": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"firstSeenAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"lastSeenAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"IP": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"userAgent": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"gameID": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"teamID": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"screenID": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "browser, organizer, team, screen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"evening_detective_serverDeviceHeartbeatReq": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"deviceID": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"connectionID": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"organizerGameID": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"teamID": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"screenID": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"accessToken": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"evening_detective_serverDeviceHeartbeatRsp": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"serverTime": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"evening_detective_serverDoor": {
|
"evening_detective_serverDoor": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -4894,6 +5043,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"evening_detective_serverGetDevicesRsp": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"devices": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/evening_detective_serverVenueDevice"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serverTime": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"evening_detective_serverGetGameAnswersRsp": {
|
"evening_detective_serverGetGameAnswersRsp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -6155,6 +6320,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"evening_detective_serverVenueDevice": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ID": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"firstSeenAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"lastSeenAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"connections": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/evening_detective_serverDeviceConnection"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"protobufAny": {
|
"protobufAny": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -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 (
|
import (
|
||||||
"evening_detective_server/internal/services/answer_service"
|
"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/file_service"
|
||||||
"evening_detective_server/internal/services/game_export_service"
|
"evening_detective_server/internal/services/game_export_service"
|
||||||
"evening_detective_server/internal/services/game_import_service"
|
"evening_detective_server/internal/services/game_import_service"
|
||||||
@@ -26,6 +27,7 @@ import (
|
|||||||
type server struct {
|
type server struct {
|
||||||
proto.UnsafeEveningDetectiveServerServer
|
proto.UnsafeEveningDetectiveServerServer
|
||||||
|
|
||||||
|
devicePresenceService *device_presence_service.DevicePresenceService
|
||||||
usersService *users_service.UsersService
|
usersService *users_service.UsersService
|
||||||
uiService *ui_service.UIService
|
uiService *ui_service.UIService
|
||||||
taskService *task_service.TaskService
|
taskService *task_service.TaskService
|
||||||
@@ -70,8 +72,10 @@ func NewServer(
|
|||||||
gameImportService *game_import_service.GameImportService,
|
gameImportService *game_import_service.GameImportService,
|
||||||
gameResultsService *game_results_package_service.GameResultsPackageService,
|
gameResultsService *game_results_package_service.GameResultsPackageService,
|
||||||
gameResultsApplyService *game_results_package_service.GameResultsApplyService,
|
gameResultsApplyService *game_results_package_service.GameResultsApplyService,
|
||||||
|
devicePresenceService *device_presence_service.DevicePresenceService,
|
||||||
) proto.EveningDetectiveServerServer {
|
) proto.EveningDetectiveServerServer {
|
||||||
return &server{
|
return &server{
|
||||||
|
devicePresenceService: devicePresenceService,
|
||||||
gameImportService: gameImportService,
|
gameImportService: gameImportService,
|
||||||
gameResultsService: gameResultsService,
|
gameResultsService: gameResultsService,
|
||||||
gameResultsApplyService: gameResultsApplyService,
|
gameResultsApplyService: gameResultsApplyService,
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
// Package device_presence_service tracks browser connections in venue memory.
|
||||||
|
package device_presence_service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"evening_detective_server/internal/services/game_model"
|
||||||
|
"evening_detective_server/internal/services/screen_service"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalid = errors.New("некорректные данные устройства")
|
||||||
|
ErrDisabled = errors.New("учёт устройств доступен только на площадке")
|
||||||
|
ErrPermission = errors.New("нет доступа к устройствам")
|
||||||
|
ErrUnauthorized = errors.New("требуется авторизация")
|
||||||
|
ErrFull = errors.New("слишком много подключений")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
connectedFor = time.Minute
|
||||||
|
retainedFor = 15 * time.Minute
|
||||||
|
maxConnections = 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
type IGamePresence interface {
|
||||||
|
GetGame(context.Context, int) (*game_model.Game, error)
|
||||||
|
}
|
||||||
|
type ITeamPresence interface {
|
||||||
|
CheckPresence(context.Context, int, string) (int, error)
|
||||||
|
}
|
||||||
|
type IScreenPresence interface {
|
||||||
|
Get(context.Context, int, string) (*screen_service.ScreenSession, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heartbeat contains transient credentials; only the verified connection is retained.
|
||||||
|
type Heartbeat struct {
|
||||||
|
DeviceID string
|
||||||
|
ConnectionID string
|
||||||
|
OrganizerGameID *int
|
||||||
|
TeamID *int
|
||||||
|
Password string
|
||||||
|
ScreenID string
|
||||||
|
ScreenTokenHash string
|
||||||
|
IP string
|
||||||
|
UserAgent string
|
||||||
|
IsAuthenticated bool
|
||||||
|
CanManage bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Connection struct {
|
||||||
|
ID string
|
||||||
|
DeviceID string
|
||||||
|
FirstSeenAt time.Time
|
||||||
|
LastSeenAt time.Time
|
||||||
|
IP string
|
||||||
|
UserAgent string
|
||||||
|
Kind string
|
||||||
|
GameID int
|
||||||
|
TeamID int
|
||||||
|
ScreenID int
|
||||||
|
Connected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Device struct {
|
||||||
|
ID string
|
||||||
|
FirstSeenAt time.Time
|
||||||
|
LastSeenAt time.Time
|
||||||
|
Connected bool
|
||||||
|
Connections []Connection
|
||||||
|
}
|
||||||
|
|
||||||
|
type DevicePresenceService struct {
|
||||||
|
enabled bool
|
||||||
|
games IGamePresence
|
||||||
|
teams ITeamPresence
|
||||||
|
screens IScreenPresence
|
||||||
|
now func() time.Time
|
||||||
|
mu sync.Mutex
|
||||||
|
connections map[string]Connection
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDevicePresenceService(enabled bool, games IGamePresence, teams ITeamPresence, screens IScreenPresence) *DevicePresenceService {
|
||||||
|
return &DevicePresenceService{
|
||||||
|
enabled: enabled,
|
||||||
|
games: games,
|
||||||
|
teams: teams,
|
||||||
|
screens: screens,
|
||||||
|
now: time.Now,
|
||||||
|
connections: make(map[string]Connection),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DevicePresenceService) Heartbeat(ctx context.Context, input Heartbeat) (bool, time.Time, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return false, time.Time{}, err
|
||||||
|
}
|
||||||
|
if !s.enabled {
|
||||||
|
return false, s.now(), nil
|
||||||
|
}
|
||||||
|
connection, err := s.verify(ctx, input)
|
||||||
|
if err != nil {
|
||||||
|
return true, time.Time{}, err
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
now := s.now()
|
||||||
|
s.prune(now)
|
||||||
|
old, exists := s.connections[connection.ID]
|
||||||
|
if exists && old.DeviceID != connection.DeviceID {
|
||||||
|
return true, now, ErrInvalid
|
||||||
|
}
|
||||||
|
if !exists && len(s.connections) >= maxConnections {
|
||||||
|
return true, now, ErrFull
|
||||||
|
}
|
||||||
|
connection.FirstSeenAt = now
|
||||||
|
if exists {
|
||||||
|
connection.FirstSeenAt = old.FirstSeenAt
|
||||||
|
}
|
||||||
|
connection.LastSeenAt = now
|
||||||
|
s.connections[connection.ID] = connection
|
||||||
|
return true, now, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DevicePresenceService) List(ctx context.Context, gameID *int, canManage bool) ([]Device, time.Time, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, time.Time{}, err
|
||||||
|
}
|
||||||
|
if !s.enabled {
|
||||||
|
return nil, time.Time{}, ErrDisabled
|
||||||
|
}
|
||||||
|
if !canManage {
|
||||||
|
return nil, time.Time{}, ErrPermission
|
||||||
|
}
|
||||||
|
if gameID != nil && *gameID <= 0 {
|
||||||
|
return nil, time.Time{}, ErrInvalid
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
now := s.now()
|
||||||
|
s.prune(now)
|
||||||
|
devices := make(map[string]*Device)
|
||||||
|
for _, connection := range s.connections {
|
||||||
|
if gameID != nil && connection.GameID != *gameID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
connection.Connected = now.Sub(connection.LastSeenAt) < connectedFor
|
||||||
|
device := devices[connection.DeviceID]
|
||||||
|
if device == nil {
|
||||||
|
device = &Device{
|
||||||
|
ID: connection.DeviceID,
|
||||||
|
FirstSeenAt: connection.FirstSeenAt,
|
||||||
|
LastSeenAt: connection.LastSeenAt,
|
||||||
|
}
|
||||||
|
devices[connection.DeviceID] = device
|
||||||
|
}
|
||||||
|
if connection.FirstSeenAt.Before(device.FirstSeenAt) {
|
||||||
|
device.FirstSeenAt = connection.FirstSeenAt
|
||||||
|
}
|
||||||
|
if connection.LastSeenAt.After(device.LastSeenAt) {
|
||||||
|
device.LastSeenAt = connection.LastSeenAt
|
||||||
|
}
|
||||||
|
device.Connected = device.Connected || connection.Connected
|
||||||
|
device.Connections = append(device.Connections, connection)
|
||||||
|
}
|
||||||
|
result := make([]Device, 0, len(devices))
|
||||||
|
for _, device := range devices {
|
||||||
|
sort.Slice(device.Connections, func(i, j int) bool { return device.Connections[i].ID < device.Connections[j].ID })
|
||||||
|
result = append(result, *device)
|
||||||
|
}
|
||||||
|
sort.Slice(result, func(i, j int) bool { return result[i].ID < result[j].ID })
|
||||||
|
return result, now, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DevicePresenceService) prune(now time.Time) {
|
||||||
|
for id, connection := range s.connections {
|
||||||
|
if now.Sub(connection.LastSeenAt) >= retainedFor {
|
||||||
|
delete(s.connections, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DevicePresenceService) verify(ctx context.Context, input Heartbeat) (Connection, error) {
|
||||||
|
deviceID, err := parseUUID(input.DeviceID)
|
||||||
|
if err != nil {
|
||||||
|
return Connection{}, err
|
||||||
|
}
|
||||||
|
connectionID, err := parseUUID(input.ConnectionID)
|
||||||
|
if err != nil {
|
||||||
|
return Connection{}, err
|
||||||
|
}
|
||||||
|
organizer := input.OrganizerGameID != nil
|
||||||
|
team := input.TeamID != nil || input.Password != ""
|
||||||
|
screen := input.ScreenID != "" || input.ScreenTokenHash != ""
|
||||||
|
contexts := 0
|
||||||
|
for _, present := range []bool{organizer, team, screen} {
|
||||||
|
if present {
|
||||||
|
contexts++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if contexts > 1 || organizer && *input.OrganizerGameID <= 0 || team && (input.TeamID == nil || *input.TeamID <= 0 || input.Password == "") || screen && (input.ScreenID == "" || input.ScreenTokenHash == "") {
|
||||||
|
return Connection{}, ErrInvalid
|
||||||
|
}
|
||||||
|
connection := Connection{
|
||||||
|
ID: connectionID,
|
||||||
|
DeviceID: deviceID,
|
||||||
|
Kind: "browser",
|
||||||
|
IP: input.IP,
|
||||||
|
UserAgent: input.UserAgent,
|
||||||
|
}
|
||||||
|
// Bound untrusted metadata as well as the number of records.
|
||||||
|
if len(connection.UserAgent) > 1024 {
|
||||||
|
connection.UserAgent = connection.UserAgent[:1024]
|
||||||
|
}
|
||||||
|
connection.UserAgent = strings.ToValidUTF8(connection.UserAgent, "")
|
||||||
|
if organizer {
|
||||||
|
if !input.IsAuthenticated {
|
||||||
|
return Connection{}, ErrUnauthorized
|
||||||
|
}
|
||||||
|
if !input.CanManage {
|
||||||
|
return Connection{}, ErrPermission
|
||||||
|
}
|
||||||
|
if _, err := s.games.GetGame(ctx, *input.OrganizerGameID); err != nil {
|
||||||
|
return Connection{}, err
|
||||||
|
}
|
||||||
|
connection.Kind = "organizer"
|
||||||
|
connection.GameID = *input.OrganizerGameID
|
||||||
|
}
|
||||||
|
if team {
|
||||||
|
gameID, err := s.teams.CheckPresence(ctx, *input.TeamID, input.Password)
|
||||||
|
if err != nil {
|
||||||
|
return Connection{}, err
|
||||||
|
}
|
||||||
|
connection.Kind = "team"
|
||||||
|
connection.TeamID = *input.TeamID
|
||||||
|
connection.GameID = gameID
|
||||||
|
}
|
||||||
|
if screen {
|
||||||
|
id, err := strconv.Atoi(input.ScreenID)
|
||||||
|
if err != nil || id <= 0 || id > 2147483647 {
|
||||||
|
return Connection{}, ErrInvalid
|
||||||
|
}
|
||||||
|
session, err := s.screens.Get(ctx, id, input.ScreenTokenHash)
|
||||||
|
if err != nil {
|
||||||
|
return Connection{}, err
|
||||||
|
}
|
||||||
|
connection.Kind = "screen"
|
||||||
|
connection.ScreenID = session.ID
|
||||||
|
if session.GameID != nil {
|
||||||
|
connection.GameID = *session.GameID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return connection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUUID(value string) (string, error) {
|
||||||
|
parsed, err := uuid.Parse(value)
|
||||||
|
if err != nil || len(value) != 36 || parsed == uuid.Nil {
|
||||||
|
return "", ErrInvalid
|
||||||
|
}
|
||||||
|
return parsed.String(), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
package device_presence_service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"evening_detective_server/internal/services/game_model"
|
||||||
|
"evening_detective_server/internal/services/game_service"
|
||||||
|
"evening_detective_server/internal/services/screen_service"
|
||||||
|
"evening_detective_server/internal/services/team_service"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type presenceFixture struct{}
|
||||||
|
|
||||||
|
func (presenceFixture) GetGame(_ context.Context, id int) (*game_model.Game, error) {
|
||||||
|
if id != 7 {
|
||||||
|
return nil, game_service.ErrGameNotFound
|
||||||
|
}
|
||||||
|
return &game_model.Game{}, nil
|
||||||
|
}
|
||||||
|
func (presenceFixture) CheckPresence(_ context.Context, id int, password string) (int, error) {
|
||||||
|
if id != 2 || password != "secret" {
|
||||||
|
return 0, team_service.ErrTeamNotFound
|
||||||
|
}
|
||||||
|
return 7, nil
|
||||||
|
}
|
||||||
|
func (presenceFixture) Get(_ context.Context, id int, hash string) (*screen_service.ScreenSession, error) {
|
||||||
|
if id != 3 || hash != "hash" {
|
||||||
|
return nil, screen_service.ErrScreenNotFound
|
||||||
|
}
|
||||||
|
gameID := 7
|
||||||
|
return &screen_service.ScreenSession{ID: 3, GameID: &gameID}, nil
|
||||||
|
}
|
||||||
|
func newFixture() (*DevicePresenceService, Heartbeat) {
|
||||||
|
fixture := presenceFixture{}
|
||||||
|
return NewDevicePresenceService(true, fixture, fixture, fixture), Heartbeat{DeviceID: uuid.NewString(), ConnectionID: uuid.NewString()}
|
||||||
|
}
|
||||||
|
func pointer(id int) *int { return &id }
|
||||||
|
func send(t *testing.T, s *DevicePresenceService, input Heartbeat) {
|
||||||
|
t.Helper()
|
||||||
|
enabled, _, err := s.Heartbeat(context.Background(), input)
|
||||||
|
if !enabled || err != nil {
|
||||||
|
t.Fatalf("heartbeat: %v %v", enabled, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func list(t *testing.T, s *DevicePresenceService, gameID *int) []Device {
|
||||||
|
t.Helper()
|
||||||
|
devices, _, err := s.List(context.Background(), gameID, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return devices
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistrationAndContext(t *testing.T) {
|
||||||
|
s, input := newFixture()
|
||||||
|
now := time.Date(2026, 10, 11, 0, 0, 0, 0, time.UTC)
|
||||||
|
s.now = func() time.Time { return now }
|
||||||
|
input.TeamID, input.Password = pointer(2), "secret"
|
||||||
|
send(t, s, input)
|
||||||
|
now = now.Add(time.Second)
|
||||||
|
input.IP, input.UserAgent = "192.0.2.1", "updated"
|
||||||
|
send(t, s, input)
|
||||||
|
devices := list(t, s, pointer(7))
|
||||||
|
if len(devices) != 1 || len(devices[0].Connections) != 1 {
|
||||||
|
t.Fatal(devices)
|
||||||
|
}
|
||||||
|
connection := devices[0].Connections[0]
|
||||||
|
if connection.IP != input.IP || connection.TeamID != 2 || connection.FirstSeenAt.Equal(connection.LastSeenAt) {
|
||||||
|
t.Fatal(connection)
|
||||||
|
}
|
||||||
|
tab := input
|
||||||
|
tab.ConnectionID = uuid.NewString()
|
||||||
|
tab.TeamID, tab.Password = nil, ""
|
||||||
|
tab.OrganizerGameID, tab.IsAuthenticated, tab.CanManage = pointer(7), true, true
|
||||||
|
send(t, s, tab)
|
||||||
|
if len(list(t, s, nil)[0].Connections) != 2 {
|
||||||
|
t.Fatal("tabs not grouped")
|
||||||
|
}
|
||||||
|
input.TeamID, input.Password = nil, ""
|
||||||
|
input.ScreenID, input.ScreenTokenHash = "3", "hash"
|
||||||
|
send(t, s, input)
|
||||||
|
connection = s.connections[input.ConnectionID]
|
||||||
|
if connection.TeamID != 0 || connection.ScreenID != 3 || connection.GameID != 7 {
|
||||||
|
t.Fatal(connection)
|
||||||
|
}
|
||||||
|
input.ScreenID, input.ScreenTokenHash = "", ""
|
||||||
|
send(t, s, input)
|
||||||
|
connection = s.connections[input.ConnectionID]
|
||||||
|
if connection.TeamID != 0 || connection.ScreenID != 0 || connection.GameID != 0 || connection.Kind != "browser" {
|
||||||
|
t.Fatal(connection)
|
||||||
|
}
|
||||||
|
if len(list(t, s, pointer(7))[0].Connections) != 1 {
|
||||||
|
t.Fatal("filter includes other contexts")
|
||||||
|
}
|
||||||
|
devices[0].Connections[0].GameID = 99
|
||||||
|
if s.connections[input.ConnectionID].GameID != 0 {
|
||||||
|
t.Fatal("list exposes registry memory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeBoundaries(t *testing.T) {
|
||||||
|
s, input := newFixture()
|
||||||
|
start := time.Now()
|
||||||
|
now := start
|
||||||
|
s.now = func() time.Time { return now }
|
||||||
|
send(t, s, input)
|
||||||
|
for _, test := range []struct {
|
||||||
|
age time.Duration
|
||||||
|
exists, connected bool
|
||||||
|
}{
|
||||||
|
{time.Minute - time.Nanosecond, true, true},
|
||||||
|
{time.Minute, true, false},
|
||||||
|
{15*time.Minute - time.Nanosecond, true, false},
|
||||||
|
{15 * time.Minute, false, false},
|
||||||
|
} {
|
||||||
|
now = start.Add(test.age)
|
||||||
|
devices := list(t, s, nil)
|
||||||
|
if (len(devices) > 0) != test.exists {
|
||||||
|
t.Fatalf("retention at %v: %v", test.age, devices)
|
||||||
|
}
|
||||||
|
if test.exists && (devices[0].Connected != test.connected || devices[0].Connections[0].Connected != test.connected) {
|
||||||
|
t.Fatalf("connection at %v", test.age)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
send(t, s, input)
|
||||||
|
now = now.Add(59 * time.Second)
|
||||||
|
tab := input
|
||||||
|
tab.ConnectionID = uuid.NewString()
|
||||||
|
send(t, s, tab)
|
||||||
|
now = now.Add(time.Second)
|
||||||
|
if !list(t, s, nil)[0].Connected {
|
||||||
|
t.Fatal("live tab must keep device connected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapacityAndPruning(t *testing.T) {
|
||||||
|
s, input := newFixture()
|
||||||
|
now := time.Now()
|
||||||
|
s.now = func() time.Time { return now }
|
||||||
|
for range maxConnections {
|
||||||
|
input.ConnectionID = uuid.NewString()
|
||||||
|
send(t, s, input)
|
||||||
|
}
|
||||||
|
send(t, s, input)
|
||||||
|
extra := input
|
||||||
|
extra.ConnectionID = uuid.NewString()
|
||||||
|
_, _, err := s.Heartbeat(context.Background(), extra)
|
||||||
|
if !errors.Is(err, ErrFull) || len(s.connections) != maxConnections {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
now = now.Add(retainedFor)
|
||||||
|
send(t, s, extra)
|
||||||
|
if len(s.connections) != 1 {
|
||||||
|
t.Fatal("expired records not pruned on update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidContextDoesNotOverwrite(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mutate func(*Heartbeat)
|
||||||
|
want error
|
||||||
|
}{
|
||||||
|
{"device UUID", func(h *Heartbeat) { h.DeviceID = "invalid" }, ErrInvalid},
|
||||||
|
{"connection UUID", func(h *Heartbeat) { h.ConnectionID = "" }, ErrInvalid},
|
||||||
|
{"nil UUID", func(h *Heartbeat) { h.DeviceID = uuid.Nil.String() }, ErrInvalid},
|
||||||
|
{"device reassignment", func(h *Heartbeat) { h.DeviceID = uuid.NewString() }, ErrInvalid},
|
||||||
|
{"mixed", func(h *Heartbeat) { h.TeamID = pointer(2); h.Password = "secret"; h.OrganizerGameID = pointer(7) }, ErrInvalid},
|
||||||
|
{"password only", func(h *Heartbeat) { h.Password = "secret" }, ErrInvalid},
|
||||||
|
{"team only", func(h *Heartbeat) { h.TeamID = pointer(2) }, ErrInvalid},
|
||||||
|
{"screen only", func(h *Heartbeat) { h.ScreenID = "3" }, ErrInvalid},
|
||||||
|
{"token only", func(h *Heartbeat) { h.ScreenTokenHash = "hash" }, ErrInvalid},
|
||||||
|
{"screen ID", func(h *Heartbeat) { h.ScreenID = "oops"; h.ScreenTokenHash = "hash" }, ErrInvalid},
|
||||||
|
{"negative game", func(h *Heartbeat) { h.OrganizerGameID = pointer(-1) }, ErrInvalid},
|
||||||
|
{"anonymous organizer", func(h *Heartbeat) { h.OrganizerGameID = pointer(7) }, ErrUnauthorized},
|
||||||
|
{"user organizer", func(h *Heartbeat) { h.OrganizerGameID = pointer(7); h.IsAuthenticated = true }, ErrPermission},
|
||||||
|
{"missing game", func(h *Heartbeat) { h.OrganizerGameID = pointer(8); h.IsAuthenticated = true; h.CanManage = true }, game_service.ErrGameNotFound},
|
||||||
|
{"bad team", func(h *Heartbeat) { h.TeamID = pointer(2); h.Password = "bad" }, team_service.ErrTeamNotFound},
|
||||||
|
{"bad screen", func(h *Heartbeat) { h.ScreenID = "3"; h.ScreenTokenHash = "bad" }, screen_service.ErrScreenNotFound},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
s, input := newFixture()
|
||||||
|
send(t, s, input)
|
||||||
|
before := s.connections[input.ConnectionID]
|
||||||
|
test.mutate(&input)
|
||||||
|
_, _, err := s.Heartbeat(context.Background(), input)
|
||||||
|
if !errors.Is(err, test.want) {
|
||||||
|
t.Fatalf("got %v, want %v", err, test.want)
|
||||||
|
}
|
||||||
|
if s.connections[before.ID] != before || len(s.connections) != 1 {
|
||||||
|
t.Fatal("invalid request modified registry")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModeAndReadAccess(t *testing.T) {
|
||||||
|
s, input := newFixture()
|
||||||
|
if _, _, err := s.List(context.Background(), nil, false); !errors.Is(err, ErrPermission) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, _, err := s.List(context.Background(), pointer(0), true); !errors.Is(err, ErrInvalid) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s.enabled = false
|
||||||
|
enabled, now, err := s.Heartbeat(context.Background(), Heartbeat{})
|
||||||
|
if enabled || now.IsZero() || err != nil || len(s.connections) != 0 {
|
||||||
|
t.Fatal("prod registered device")
|
||||||
|
}
|
||||||
|
if _, _, err = s.List(context.Background(), nil, true); !errors.Is(err, ErrDisabled) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
if _, _, err = s.Heartbeat(ctx, input); !errors.Is(err, context.Canceled) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrentAccess(t *testing.T) {
|
||||||
|
s, input := newFixture()
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for range 20 {
|
||||||
|
wg.Go(func() {
|
||||||
|
tab := input
|
||||||
|
tab.ConnectionID = uuid.NewString()
|
||||||
|
for range 50 {
|
||||||
|
send(t, s, tab)
|
||||||
|
list(t, s, nil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
if len(list(t, s, nil)[0].Connections) != 20 {
|
||||||
|
t.Fatal("lost connections")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetadataUTF8(t *testing.T) {
|
||||||
|
for _, agent := range []string{strings.Repeat("x", 1023) + "я", "bad\xffagent"} {
|
||||||
|
s, input := newFixture()
|
||||||
|
input.UserAgent = agent
|
||||||
|
send(t, s, input)
|
||||||
|
stored := list(t, s, nil)[0].Connections[0].UserAgent
|
||||||
|
if len(stored) > 1024 || !utf8.ValidString(stored) {
|
||||||
|
t.Fatal("metadata must serialize safely")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -377,7 +377,7 @@ func verifyExportedGameReads(t *testing.T, f serviceFixture, worker *game_export
|
|||||||
games := game_service.NewGameService(f.games, f.teams, f.scenarios, nil, "", actions_repo.NewActionsRepo(f.pool), applications_repo.NewApplicationsRepo(f.pool))
|
games := game_service.NewGameService(f.games, f.teams, f.scenarios, nil, "", actions_repo.NewActionsRepo(f.pool), applications_repo.NewApplicationsRepo(f.pool))
|
||||||
mapRepo := game_map_repo.NewGameMapRepo(f.pool)
|
mapRepo := game_map_repo.NewGameMapRepo(f.pool)
|
||||||
server := app.NewServer(nil, nil, nil, nil, games, nil, nil, nil, nil, nil, nil,
|
server := app.NewServer(nil, nil, nil, nil, games, nil, nil, nil, nil, nil, nil,
|
||||||
screen_service.NewScreenService(screen_sessions_repo.NewScreenSessionsRepo(f.pool)), nil, nil, map_service.NewMapService(mapRepo), nil, worker, nil, nil, nil)
|
screen_service.NewScreenService(screen_sessions_repo.NewScreenSessionsRepo(f.pool)), nil, nil, map_service.NewMapService(mapRepo), nil, worker, nil, nil, nil, nil)
|
||||||
ctx := processor_jwt.ContextWithClaims(context.Background(), &processor_jwt.JWTClaims{UserID: f.userID, Roles: []string{"organizer"}})
|
ctx := processor_jwt.ContextWithClaims(context.Background(), &processor_jwt.JWTClaims{UserID: f.userID, Roles: []string{"organizer"}})
|
||||||
game, err := server.GetGame(ctx, &proto.GetGameReq{ID: int32(f.gameID)})
|
game, err := server.GetGame(ctx, &proto.GetGameReq{ID: int32(f.gameID)})
|
||||||
check(t, err)
|
check(t, err)
|
||||||
|
|||||||
@@ -166,3 +166,18 @@ func (s *TeamService) DownloadTeamQR(ctx context.Context, id int) ([]byte, strin
|
|||||||
data, err := team_qr_pdf.Generate(cards)
|
data, err := team_qr_pdf.Generate(cards)
|
||||||
return data, filename, err
|
return data, filename, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckPresence validates team credentials without loading game progress or a scenario.
|
||||||
|
func (s *TeamService) CheckPresence(ctx context.Context, teamID int, password string) (int, error) {
|
||||||
|
team, err := s.teamsRepo.GetTeamByID(ctx, teamID)
|
||||||
|
if errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||||
|
return 0, ErrTeamNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if team.Password != password || team.ParticipationStatus == "cancelled" {
|
||||||
|
return 0, ErrTeamNotFound
|
||||||
|
}
|
||||||
|
return team.GameID, nil
|
||||||
|
}
|
||||||
|
|||||||
+755
-246
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,68 @@ var (
|
|||||||
_ = metadata.Join
|
_ = metadata.Join
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func request_EveningDetectiveServer_DeviceHeartbeat_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||||
|
var (
|
||||||
|
protoReq DeviceHeartbeatReq
|
||||||
|
metadata runtime.ServerMetadata
|
||||||
|
)
|
||||||
|
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||||
|
}
|
||||||
|
if req.Body != nil {
|
||||||
|
_, _ = io.Copy(io.Discard, req.Body)
|
||||||
|
}
|
||||||
|
msg, err := client.DeviceHeartbeat(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||||
|
return msg, metadata, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func local_request_EveningDetectiveServer_DeviceHeartbeat_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||||
|
var (
|
||||||
|
protoReq DeviceHeartbeatReq
|
||||||
|
metadata runtime.ServerMetadata
|
||||||
|
)
|
||||||
|
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||||
|
}
|
||||||
|
msg, err := server.DeviceHeartbeat(ctx, &protoReq)
|
||||||
|
return msg, metadata, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var filter_EveningDetectiveServer_GetDevices_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
|
||||||
|
|
||||||
|
func request_EveningDetectiveServer_GetDevices_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||||
|
var (
|
||||||
|
protoReq GetDevicesReq
|
||||||
|
metadata runtime.ServerMetadata
|
||||||
|
)
|
||||||
|
if req.Body != nil {
|
||||||
|
_, _ = io.Copy(io.Discard, req.Body)
|
||||||
|
}
|
||||||
|
if err := req.ParseForm(); err != nil {
|
||||||
|
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||||
|
}
|
||||||
|
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_EveningDetectiveServer_GetDevices_0); err != nil {
|
||||||
|
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||||
|
}
|
||||||
|
msg, err := client.GetDevices(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||||
|
return msg, metadata, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func local_request_EveningDetectiveServer_GetDevices_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||||
|
var (
|
||||||
|
protoReq GetDevicesReq
|
||||||
|
metadata runtime.ServerMetadata
|
||||||
|
)
|
||||||
|
if err := req.ParseForm(); err != nil {
|
||||||
|
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||||
|
}
|
||||||
|
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_EveningDetectiveServer_GetDevices_0); err != nil {
|
||||||
|
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||||
|
}
|
||||||
|
msg, err := server.GetDevices(ctx, &protoReq)
|
||||||
|
return msg, metadata, err
|
||||||
|
}
|
||||||
|
|
||||||
func request_EveningDetectiveServer_GetTasks_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
func request_EveningDetectiveServer_GetTasks_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||||
var (
|
var (
|
||||||
protoReq GetTasksReq
|
protoReq GetTasksReq
|
||||||
@@ -4235,6 +4297,46 @@ func local_request_EveningDetectiveServer_SetTeamAnswerScore_0(ctx context.Conte
|
|||||||
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterEveningDetectiveServerHandlerFromEndpoint instead.
|
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterEveningDetectiveServerHandlerFromEndpoint instead.
|
||||||
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
|
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
|
||||||
func RegisterEveningDetectiveServerHandlerServer(ctx context.Context, mux *runtime.ServeMux, server EveningDetectiveServerServer) error {
|
func RegisterEveningDetectiveServerHandlerServer(ctx context.Context, mux *runtime.ServeMux, server EveningDetectiveServerServer) error {
|
||||||
|
mux.Handle(http.MethodPost, pattern_EveningDetectiveServer_DeviceHeartbeat_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
|
defer cancel()
|
||||||
|
var stream runtime.ServerTransportStream
|
||||||
|
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||||
|
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||||
|
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/DeviceHeartbeat", runtime.WithHTTPPathPattern("/api/devices/heartbeat"))
|
||||||
|
if err != nil {
|
||||||
|
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, md, err := local_request_EveningDetectiveServer_DeviceHeartbeat_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||||
|
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||||
|
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||||
|
if err != nil {
|
||||||
|
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
forward_EveningDetectiveServer_DeviceHeartbeat_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||||
|
})
|
||||||
|
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetDevices_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
|
defer cancel()
|
||||||
|
var stream runtime.ServerTransportStream
|
||||||
|
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||||
|
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||||
|
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/GetDevices", runtime.WithHTTPPathPattern("/api/devices"))
|
||||||
|
if err != nil {
|
||||||
|
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, md, err := local_request_EveningDetectiveServer_GetDevices_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
||||||
|
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||||
|
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||||
|
if err != nil {
|
||||||
|
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
forward_EveningDetectiveServer_GetDevices_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||||
|
})
|
||||||
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetTasks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetTasks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
ctx, cancel := context.WithCancel(req.Context())
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -6455,6 +6557,40 @@ func RegisterEveningDetectiveServerHandler(ctx context.Context, mux *runtime.Ser
|
|||||||
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
|
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
|
||||||
// "EveningDetectiveServerClient" to call the correct interceptors. This client ignores the HTTP middlewares.
|
// "EveningDetectiveServerClient" to call the correct interceptors. This client ignores the HTTP middlewares.
|
||||||
func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runtime.ServeMux, client EveningDetectiveServerClient) error {
|
func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runtime.ServeMux, client EveningDetectiveServerClient) error {
|
||||||
|
mux.Handle(http.MethodPost, pattern_EveningDetectiveServer_DeviceHeartbeat_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
|
defer cancel()
|
||||||
|
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||||
|
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/DeviceHeartbeat", runtime.WithHTTPPathPattern("/api/devices/heartbeat"))
|
||||||
|
if err != nil {
|
||||||
|
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, md, err := request_EveningDetectiveServer_DeviceHeartbeat_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||||
|
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||||
|
if err != nil {
|
||||||
|
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
forward_EveningDetectiveServer_DeviceHeartbeat_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||||
|
})
|
||||||
|
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetDevices_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
|
defer cancel()
|
||||||
|
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||||
|
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/GetDevices", runtime.WithHTTPPathPattern("/api/devices"))
|
||||||
|
if err != nil {
|
||||||
|
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, md, err := request_EveningDetectiveServer_GetDevices_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
||||||
|
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
||||||
|
if err != nil {
|
||||||
|
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
forward_EveningDetectiveServer_GetDevices_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||||
|
})
|
||||||
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetTasks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetTasks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
ctx, cancel := context.WithCancel(req.Context())
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -8312,6 +8448,8 @@ func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runti
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
pattern_EveningDetectiveServer_DeviceHeartbeat_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "devices", "heartbeat"}, ""))
|
||||||
|
pattern_EveningDetectiveServer_GetDevices_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "devices"}, ""))
|
||||||
pattern_EveningDetectiveServer_GetTasks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "tasks"}, ""))
|
pattern_EveningDetectiveServer_GetTasks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "tasks"}, ""))
|
||||||
pattern_EveningDetectiveServer_GetTaskAssignees_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "tasks", "assignees"}, ""))
|
pattern_EveningDetectiveServer_GetTaskAssignees_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "tasks", "assignees"}, ""))
|
||||||
pattern_EveningDetectiveServer_CreateTask_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "tasks"}, ""))
|
pattern_EveningDetectiveServer_CreateTask_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "tasks"}, ""))
|
||||||
@@ -8424,6 +8562,8 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
forward_EveningDetectiveServer_DeviceHeartbeat_0 = runtime.ForwardResponseMessage
|
||||||
|
forward_EveningDetectiveServer_GetDevices_0 = runtime.ForwardResponseMessage
|
||||||
forward_EveningDetectiveServer_GetTasks_0 = runtime.ForwardResponseMessage
|
forward_EveningDetectiveServer_GetTasks_0 = runtime.ForwardResponseMessage
|
||||||
forward_EveningDetectiveServer_GetTaskAssignees_0 = runtime.ForwardResponseMessage
|
forward_EveningDetectiveServer_GetTaskAssignees_0 = runtime.ForwardResponseMessage
|
||||||
forward_EveningDetectiveServer_CreateTask_0 = runtime.ForwardResponseMessage
|
forward_EveningDetectiveServer_CreateTask_0 = runtime.ForwardResponseMessage
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
EveningDetectiveServer_DeviceHeartbeat_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DeviceHeartbeat"
|
||||||
|
EveningDetectiveServer_GetDevices_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetDevices"
|
||||||
EveningDetectiveServer_GetTasks_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetTasks"
|
EveningDetectiveServer_GetTasks_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetTasks"
|
||||||
EveningDetectiveServer_GetTaskAssignees_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetTaskAssignees"
|
EveningDetectiveServer_GetTaskAssignees_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetTaskAssignees"
|
||||||
EveningDetectiveServer_CreateTask_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/CreateTask"
|
EveningDetectiveServer_CreateTask_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/CreateTask"
|
||||||
@@ -141,6 +143,8 @@ const (
|
|||||||
//
|
//
|
||||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
type EveningDetectiveServerClient interface {
|
type EveningDetectiveServerClient interface {
|
||||||
|
DeviceHeartbeat(ctx context.Context, in *DeviceHeartbeatReq, opts ...grpc.CallOption) (*DeviceHeartbeatRsp, error)
|
||||||
|
GetDevices(ctx context.Context, in *GetDevicesReq, opts ...grpc.CallOption) (*GetDevicesRsp, error)
|
||||||
GetTasks(ctx context.Context, in *GetTasksReq, opts ...grpc.CallOption) (*GetTasksRsp, error)
|
GetTasks(ctx context.Context, in *GetTasksReq, opts ...grpc.CallOption) (*GetTasksRsp, error)
|
||||||
GetTaskAssignees(ctx context.Context, in *GetTaskAssigneesReq, opts ...grpc.CallOption) (*GetTaskAssigneesRsp, error)
|
GetTaskAssignees(ctx context.Context, in *GetTaskAssigneesReq, opts ...grpc.CallOption) (*GetTaskAssigneesRsp, error)
|
||||||
CreateTask(ctx context.Context, in *CreateTaskReq, opts ...grpc.CallOption) (*TaskRsp, error)
|
CreateTask(ctx context.Context, in *CreateTaskReq, opts ...grpc.CallOption) (*TaskRsp, error)
|
||||||
@@ -272,6 +276,26 @@ func NewEveningDetectiveServerClient(cc grpc.ClientConnInterface) EveningDetecti
|
|||||||
return &eveningDetectiveServerClient{cc}
|
return &eveningDetectiveServerClient{cc}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *eveningDetectiveServerClient) DeviceHeartbeat(ctx context.Context, in *DeviceHeartbeatReq, opts ...grpc.CallOption) (*DeviceHeartbeatRsp, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(DeviceHeartbeatRsp)
|
||||||
|
err := c.cc.Invoke(ctx, EveningDetectiveServer_DeviceHeartbeat_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *eveningDetectiveServerClient) GetDevices(ctx context.Context, in *GetDevicesReq, opts ...grpc.CallOption) (*GetDevicesRsp, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetDevicesRsp)
|
||||||
|
err := c.cc.Invoke(ctx, EveningDetectiveServer_GetDevices_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *eveningDetectiveServerClient) GetTasks(ctx context.Context, in *GetTasksReq, opts ...grpc.CallOption) (*GetTasksRsp, error) {
|
func (c *eveningDetectiveServerClient) GetTasks(ctx context.Context, in *GetTasksReq, opts ...grpc.CallOption) (*GetTasksRsp, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(GetTasksRsp)
|
out := new(GetTasksRsp)
|
||||||
@@ -1366,6 +1390,8 @@ func (c *eveningDetectiveServerClient) SetTeamAnswerScore(ctx context.Context, i
|
|||||||
// All implementations must embed UnimplementedEveningDetectiveServerServer
|
// All implementations must embed UnimplementedEveningDetectiveServerServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
type EveningDetectiveServerServer interface {
|
type EveningDetectiveServerServer interface {
|
||||||
|
DeviceHeartbeat(context.Context, *DeviceHeartbeatReq) (*DeviceHeartbeatRsp, error)
|
||||||
|
GetDevices(context.Context, *GetDevicesReq) (*GetDevicesRsp, error)
|
||||||
GetTasks(context.Context, *GetTasksReq) (*GetTasksRsp, error)
|
GetTasks(context.Context, *GetTasksReq) (*GetTasksRsp, error)
|
||||||
GetTaskAssignees(context.Context, *GetTaskAssigneesReq) (*GetTaskAssigneesRsp, error)
|
GetTaskAssignees(context.Context, *GetTaskAssigneesReq) (*GetTaskAssigneesRsp, error)
|
||||||
CreateTask(context.Context, *CreateTaskReq) (*TaskRsp, error)
|
CreateTask(context.Context, *CreateTaskReq) (*TaskRsp, error)
|
||||||
@@ -1497,6 +1523,12 @@ type EveningDetectiveServerServer interface {
|
|||||||
// pointer dereference when methods are called.
|
// pointer dereference when methods are called.
|
||||||
type UnimplementedEveningDetectiveServerServer struct{}
|
type UnimplementedEveningDetectiveServerServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedEveningDetectiveServerServer) DeviceHeartbeat(context.Context, *DeviceHeartbeatReq) (*DeviceHeartbeatRsp, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method DeviceHeartbeat not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedEveningDetectiveServerServer) GetDevices(context.Context, *GetDevicesReq) (*GetDevicesRsp, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method GetDevices not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedEveningDetectiveServerServer) GetTasks(context.Context, *GetTasksReq) (*GetTasksRsp, error) {
|
func (UnimplementedEveningDetectiveServerServer) GetTasks(context.Context, *GetTasksReq) (*GetTasksRsp, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method GetTasks not implemented")
|
return nil, status.Error(codes.Unimplemented, "method GetTasks not implemented")
|
||||||
}
|
}
|
||||||
@@ -1846,6 +1878,42 @@ func RegisterEveningDetectiveServerServer(s grpc.ServiceRegistrar, srv EveningDe
|
|||||||
s.RegisterService(&EveningDetectiveServer_ServiceDesc, srv)
|
s.RegisterService(&EveningDetectiveServer_ServiceDesc, srv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _EveningDetectiveServer_DeviceHeartbeat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(DeviceHeartbeatReq)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(EveningDetectiveServerServer).DeviceHeartbeat(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: EveningDetectiveServer_DeviceHeartbeat_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(EveningDetectiveServerServer).DeviceHeartbeat(ctx, req.(*DeviceHeartbeatReq))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _EveningDetectiveServer_GetDevices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetDevicesReq)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(EveningDetectiveServerServer).GetDevices(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: EveningDetectiveServer_GetDevices_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(EveningDetectiveServerServer).GetDevices(ctx, req.(*GetDevicesReq))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _EveningDetectiveServer_GetTasks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _EveningDetectiveServer_GetTasks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(GetTasksReq)
|
in := new(GetTasksReq)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -3815,6 +3883,14 @@ var EveningDetectiveServer_ServiceDesc = grpc.ServiceDesc{
|
|||||||
ServiceName: "crabs.evening_detective_server.EveningDetectiveServer",
|
ServiceName: "crabs.evening_detective_server.EveningDetectiveServer",
|
||||||
HandlerType: (*EveningDetectiveServerServer)(nil),
|
HandlerType: (*EveningDetectiveServerServer)(nil),
|
||||||
Methods: []grpc.MethodDesc{
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "DeviceHeartbeat",
|
||||||
|
Handler: _EveningDetectiveServer_DeviceHeartbeat_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetDevices",
|
||||||
|
Handler: _EveningDetectiveServer_GetDevices_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "GetTasks",
|
MethodName: "GetTasks",
|
||||||
Handler: _EveningDetectiveServer_GetTasks_Handler,
|
Handler: _EveningDetectiveServer_GetTasks_Handler,
|
||||||
|
|||||||
Reference in New Issue
Block a user