diff --git a/api/main.proto b/api/main.proto index 20b4be2..3639752 100644 --- a/api/main.proto +++ b/api/main.proto @@ -44,48 +44,48 @@ service EveningDetectiveServer { rpc GetTasks(GetTasksReq) returns (GetTasksRsp) { option (google.api.http) = { get: "/api/tasks" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Задачи"; - summary: "Получить общую доску задач"; + tags : "Задачи"; + summary : "Получить общую доску задач"; description: "Доступно разработчикам и администратору."; }; } rpc GetTaskAssignees(GetTaskAssigneesReq) returns (GetTaskAssigneesRsp) { option (google.api.http) = { get: "/api/tasks/assignees" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Задачи"; - summary: "Получить разработчиков для назначения"; + tags : "Задачи"; + summary : "Получить разработчиков для назначения"; description: "Доступно разработчикам и администратору."; }; } rpc CreateTask(CreateTaskReq) returns (TaskRsp) { option (google.api.http) = { post: "/api/tasks" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Задачи"; - summary: "Создать задачу"; + tags : "Задачи"; + summary : "Создать задачу"; description: "Доступно разработчикам и администратору."; }; } rpc UpdateTask(UpdateTaskReq) returns (TaskRsp) { option (google.api.http) = { put: "/api/tasks/{ID}" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Задачи"; - summary: "Редактировать задачу"; + tags : "Задачи"; + summary : "Редактировать задачу"; description: "Доступно разработчикам и администратору."; }; } rpc SetTaskStatus(SetTaskStatusReq) returns (TaskRsp) { option (google.api.http) = { post: "/api/tasks/{ID}/status" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Задачи"; - summary: "Изменить статус задачи"; + tags : "Задачи"; + summary : "Изменить статус задачи"; description: "Доступно разработчикам и администратору."; }; } rpc DeleteTask(DeleteTaskReq) returns (DeleteTaskRsp) { option (google.api.http) = { post: "/api/tasks/{ID}/delete" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Задачи"; - summary: "Удалить задачу"; + tags : "Задачи"; + summary : "Удалить задачу"; description: "Доступно разработчикам и администратору."; }; } @@ -1275,6 +1275,8 @@ message GetPermissionsReq {} message GetPermissionsRsp { repeated string permissions = 1; + // Actual server environment: prod or venue. + string environment = 2; } message UploadFileReq { @@ -2239,53 +2241,64 @@ message TicketPage { // Developer tasks use new, in_progress, review and done statuses. message DeveloperTask { - int32 ID = 1; - string title = 2; + int32 ID = 1; + string title = 2; string description = 3; - string status = 4; + string status = 4; // Omitted assignee means the task is unassigned. - optional int32 assigneeID = 5; - string assigneeName = 6; - optional int32 authorID = 7; + optional int32 assigneeID = 5; + string assigneeName = 6; + optional int32 authorID = 7; // Calendar deadline in YYYY-MM-DD format; empty means no deadline. - string dueDate = 8; + string dueDate = 8; google.protobuf.Timestamp createdAt = 9; google.protobuf.Timestamp updatedAt = 10; // Required for updates and deletion to detect concurrent edits. int32 version = 11; } + message TaskAssignee { - int32 ID = 1; + int32 ID = 1; string name = 2; } + message GetTasksReq {} + message GetTasksRsp { repeated DeveloperTask tasks = 1; } + message GetTaskAssigneesReq {} + message GetTaskAssigneesRsp { repeated TaskAssignee assignees = 1; } + message CreateTaskReq { - string title = 1; - string description = 2; - string status = 3; - optional int32 assigneeID = 4; - string dueDate = 5; + string title = 1; + string description = 2; + string status = 3; + optional int32 assigneeID = 4; + string dueDate = 5; } + message UpdateTaskReq { - int32 ID = 1; - string title = 2; - string description = 3; - string status = 4; - optional int32 assigneeID = 5; - string dueDate = 6; - int32 version = 7; + int32 ID = 1; + string title = 2; + string description = 3; + string status = 4; + optional int32 assigneeID = 5; + string dueDate = 6; + int32 version = 7; } + message SetTaskStatusReq { - int32 ID = 1; - string status = 2; - int32 version = 3; + int32 ID = 1; + string status = 2; + int32 version = 3; } + message DeleteTaskReq { - int32 ID = 1; + int32 ID = 1; int32 version = 2; } + message TaskRsp { DeveloperTask task = 1; } + message DeleteTaskRsp {} diff --git a/bin/evening_detective_server b/bin/evening_detective_server index 578089e..938b5d7 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 defc40c..82624be 100644 --- a/cmd/evening_detective_server/main.go +++ b/cmd/evening_detective_server/main.go @@ -115,7 +115,7 @@ func main() { refreshTokensRepo := refresh_tokens_repo.NewRefreshTokensRepo(dbpool) usersService := users_service.NewUsersService(usersRepo, passwordGenerator, emailSender, processorJWT, refreshTokensRepo, os.Getenv("APP_BASE_URL")) taskService := task_service.NewTaskService(tasks_repo.NewTasksRepo(dbpool)) - uiService := ui_service.NewUIService() + uiService := ui_service.NewUIService(venueMode) fileStorage, err := file_storage.NewRustFSStorage( context.Background(), os.Getenv("S3_HOST"), diff --git a/cmd/evening_detective_server/main.swagger.json b/cmd/evening_detective_server/main.swagger.json index a7e530b..c65c385 100644 --- a/cmd/evening_detective_server/main.swagger.json +++ b/cmd/evening_detective_server/main.swagger.json @@ -4802,6 +4802,10 @@ "items": { "type": "string" } + }, + "environment": { + "type": "string", + "description": "Actual server environment: prod or venue." } } }, diff --git a/internal/app/users.go b/internal/app/users.go index df91227..5d88f57 100644 --- a/internal/app/users.go +++ b/internal/app/users.go @@ -203,14 +203,14 @@ func (s *server) DeleteUserRole(ctx context.Context, req *proto.DeleteUserRoleRe } func (s *server) GetPermissions(ctx context.Context, _ *proto.GetPermissionsReq) (*proto.GetPermissionsRsp, error) { - claims, _ := processor_jwt.ClaimsFromContext(ctx) - if claims == nil { - return &proto.GetPermissionsRsp{}, nil + rsp := &proto.GetPermissionsRsp{ + Environment: s.uiService.Environment, } - permissions := s.uiService.GetPermissions(claims.Roles) - return &proto.GetPermissionsRsp{ - Permissions: permissions, - }, nil + claims, _ := processor_jwt.ClaimsFromContext(ctx) + if claims != nil { + rsp.Permissions = s.uiService.GetPermissions(claims.Roles) + } + return rsp, nil } func (s *server) ChangeUserPassword(ctx context.Context, req *proto.ChangeUserPasswordReq) (*proto.ChangeUserPasswordRsp, error) { diff --git a/internal/app/users_permissions_test.go b/internal/app/users_permissions_test.go index 9119f86..790594d 100644 --- a/internal/app/users_permissions_test.go +++ b/internal/app/users_permissions_test.go @@ -9,32 +9,29 @@ import ( proto "evening_detective_server/proto" ) -func TestGetPermissionsAllowsAnonymousUsers(t *testing.T) { - s := &server{uiService: ui_service.NewUIService()} - - rsp, err := s.GetPermissions(context.Background(), &proto.GetPermissionsReq{}) - if err != nil { - t.Fatalf("GetPermissions() error = %v", err) - } - if len(rsp.GetPermissions()) != 0 { - t.Fatalf("anonymous permissions = %v, want empty", rsp.GetPermissions()) - } - - ctx := processor_jwt.ContextWithClaims(context.Background(), &processor_jwt.JWTClaims{Roles: []string{"user"}}) - rsp, err = s.GetPermissions(ctx, &proto.GetPermissionsReq{}) - if err != nil { - t.Fatalf("GetPermissions() with claims error = %v", err) - } - if len(rsp.GetPermissions()) == 0 { - t.Fatal("authenticated permissions are empty") - } - - ctx = processor_jwt.ContextWithClaims(context.Background(), &processor_jwt.JWTClaims{Roles: []string{"admin"}}) - rsp, err = s.GetPermissions(ctx, &proto.GetPermissionsReq{}) - if err != nil { - t.Fatalf("GetPermissions() for admin error = %v", err) - } - if len(rsp.GetPermissions()) == 0 { - t.Fatal("admin permissions are empty") +func TestGetPermissionsEnvironment(t *testing.T) { + for _, mode := range []struct { + name string + venue bool + }{{"prod", false}, {"venue", true}} { + for _, role := range []string{"", "user", "admin"} { + t.Run(mode.name+"/"+role, func(t *testing.T) { + s := &server{uiService: ui_service.NewUIService(mode.venue)} + ctx := context.Background() + if role != "" { + ctx = processor_jwt.ContextWithClaims(ctx, &processor_jwt.JWTClaims{Roles: []string{role}}) + } + rsp, err := s.GetPermissions(ctx, &proto.GetPermissionsReq{}) + if err != nil { + t.Fatalf("GetPermissions() error = %v", err) + } + if rsp.GetEnvironment() != mode.name { + t.Fatalf("environment = %q, want %q", rsp.GetEnvironment(), mode.name) + } + if (len(rsp.GetPermissions()) == 0) != (role == "") { + t.Fatalf("unexpected permissions for role %q: %v", role, rsp.GetPermissions()) + } + }) + } } } diff --git a/internal/services/ui_service/service.go b/internal/services/ui_service/service.go index 5f8e396..4f61f42 100644 --- a/internal/services/ui_service/service.go +++ b/internal/services/ui_service/service.go @@ -32,11 +32,17 @@ var ( ) // UIService provides user interface permissions. -type UIService struct{} +type UIService struct { + Environment string +} // NewUIService creates a UI service. -func NewUIService() *UIService { - return &UIService{} +func NewUIService(venueMode bool) *UIService { + environment := "prod" + if venueMode { + environment = "venue" + } + return &UIService{Environment: environment} } // GetPermissions returns permissions for the supplied roles. diff --git a/internal/services/ui_service/service_test.go b/internal/services/ui_service/service_test.go index d1dd022..658218f 100644 --- a/internal/services/ui_service/service_test.go +++ b/internal/services/ui_service/service_test.go @@ -13,7 +13,7 @@ func TestTasksPermission(t *testing.T) { {"developer", true}, {"admin", true}, {"user", false}, {"author", false}, {"organizer", false}, } { t.Run(tc.role, func(t *testing.T) { - got := NewUIService().GetPermissions([]string{tc.role}) + got := NewUIService(false).GetPermissions([]string{tc.role}) if slices.Contains(got, "tasks_page") != tc.allowed { t.Fatalf("permissions=%v", got) } diff --git a/proto/main.pb.go b/proto/main.pb.go index 9d8e5d7..2faa8ea 100644 --- a/proto/main.pb.go +++ b/proto/main.pb.go @@ -1776,8 +1776,10 @@ func (*GetPermissionsReq) Descriptor() ([]byte, []int) { } type GetPermissionsRsp struct { - state protoimpl.MessageState `protogen:"open.v1"` - Permissions []string `protobuf:"bytes,1,rep,name=permissions,proto3" json:"permissions,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Permissions []string `protobuf:"bytes,1,rep,name=permissions,proto3" json:"permissions,omitempty"` + // Actual server environment: prod or venue. + Environment string `protobuf:"bytes,2,opt,name=environment,proto3" json:"environment,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1819,6 +1821,13 @@ func (x *GetPermissionsRsp) GetPermissions() []string { return nil } +func (x *GetPermissionsRsp) GetEnvironment() string { + if x != nil { + return x.Environment + } + return "" +} + type UploadFileReq struct { state protoimpl.MessageState `protogen:"open.v1"` Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` @@ -11839,9 +11848,10 @@ const file_main_proto_rawDesc = "" + "\x02ID\x18\x01 \x01(\x05R\x02ID\x12\x12\n" + "\x04role\x18\x02 \x01(\tR\x04role\"\x13\n" + "\x11DeleteUserRoleRsp\"\x13\n" + - "\x11GetPermissionsReq\"5\n" + + "\x11GetPermissionsReq\"W\n" + "\x11GetPermissionsRsp\x12 \n" + - "\vpermissions\x18\x01 \x03(\tR\vpermissions\"?\n" + + "\vpermissions\x18\x01 \x03(\tR\vpermissions\x12 \n" + + "\venvironment\x18\x02 \x01(\tR\venvironment\"?\n" + "\rUploadFileReq\x12\x1a\n" + "\bfilename\x18\x01 \x01(\tR\bfilename\x12\x12\n" + "\x04data\x18\x02 \x01(\fR\x04data\"I\n" +