From be060361aa1506606ef05792f246044816383b07 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Sun, 1 Feb 2026 04:52:39 +0100 Subject: [PATCH 1/3] feat(organizations): add AdminSetPlan RPC and messages Introduce AdminSetPlanRequest and AdminSetPlanResponse protobuf types and register a new OrganizationService RPC AdminSetPlan. The request carries organization_id and plan_id (plan_id must match an OrganizationPlan.ID). The response returns the updated Organization and the applied plan_id. This enables superadmin users to set or change an organization's active plan via the gRPC API. Update the generated .pb.go descriptors and file raw descriptor to include the new messages and service method. --- .../deployments/v1/deployment_service.pb.go | 2 +- .../v1/organization_service.pb.go | 236 +++++++++++++----- .../organization_service.connect.go | 31 +++ .../v1/organization_service.proto | 44 ++-- .../deployments/v1/deployment_service_pb.ts | 2 +- .../v1/organization_service_pb.ts | 62 ++++- 6 files changed, 300 insertions(+), 77 deletions(-) diff --git a/apps/shared/proto/obiente/cloud/deployments/v1/deployment_service.pb.go b/apps/shared/proto/obiente/cloud/deployments/v1/deployment_service.pb.go index d8528391..55dcb73f 100644 --- a/apps/shared/proto/obiente/cloud/deployments/v1/deployment_service.pb.go +++ b/apps/shared/proto/obiente/cloud/deployments/v1/deployment_service.pb.go @@ -333,7 +333,7 @@ func (BuildStatus) EnumDescriptor() ([]byte, []int) { type HealthCheckType int32 const ( - HealthCheckType_HEALTHCHECK_TYPE_UNSPECIFIED HealthCheckType = 0 // No health check + HealthCheckType_HEALTHCHECK_TYPE_UNSPECIFIED HealthCheckType = 0 // Auto-detect (TCP if routing exists, otherwise no healthcheck) HealthCheckType_HEALTHCHECK_DISABLED HealthCheckType = 1 // Explicitly disabled HealthCheckType_HEALTHCHECK_TCP HealthCheckType = 2 // TCP port check (nc) HealthCheckType_HEALTHCHECK_HTTP HealthCheckType = 3 // HTTP endpoint check diff --git a/apps/shared/proto/obiente/cloud/organizations/v1/organization_service.pb.go b/apps/shared/proto/obiente/cloud/organizations/v1/organization_service.pb.go index 5d12844e..84eaa2db 100644 --- a/apps/shared/proto/obiente/cloud/organizations/v1/organization_service.pb.go +++ b/apps/shared/proto/obiente/cloud/organizations/v1/organization_service.pb.go @@ -2847,6 +2847,112 @@ func (x *GetMyPermissionsResponse) GetPermissions() []string { return nil } +// Request to set the active plan for an organization (superadmin only) +type AdminSetPlanRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrganizationId string `protobuf:"bytes,1,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` + PlanId string `protobuf:"bytes,2,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` // The plan to assign (must match OrganizationPlan.ID) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminSetPlanRequest) Reset() { + *x = AdminSetPlanRequest{} + mi := &file_obiente_cloud_organizations_v1_organization_service_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminSetPlanRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminSetPlanRequest) ProtoMessage() {} + +func (x *AdminSetPlanRequest) ProtoReflect() protoreflect.Message { + mi := &file_obiente_cloud_organizations_v1_organization_service_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdminSetPlanRequest.ProtoReflect.Descriptor instead. +func (*AdminSetPlanRequest) Descriptor() ([]byte, []int) { + return file_obiente_cloud_organizations_v1_organization_service_proto_rawDescGZIP(), []int{45} +} + +func (x *AdminSetPlanRequest) GetOrganizationId() string { + if x != nil { + return x.OrganizationId + } + return "" +} + +func (x *AdminSetPlanRequest) GetPlanId() string { + if x != nil { + return x.PlanId + } + return "" +} + +// Response for AdminSetPlan +type AdminSetPlanResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Organization *Organization `protobuf:"bytes,1,opt,name=organization,proto3" json:"organization,omitempty"` + PlanId string `protobuf:"bytes,2,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminSetPlanResponse) Reset() { + *x = AdminSetPlanResponse{} + mi := &file_obiente_cloud_organizations_v1_organization_service_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminSetPlanResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminSetPlanResponse) ProtoMessage() {} + +func (x *AdminSetPlanResponse) ProtoReflect() protoreflect.Message { + mi := &file_obiente_cloud_organizations_v1_organization_service_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdminSetPlanResponse.ProtoReflect.Descriptor instead. +func (*AdminSetPlanResponse) Descriptor() ([]byte, []int) { + return file_obiente_cloud_organizations_v1_organization_service_proto_rawDescGZIP(), []int{46} +} + +func (x *AdminSetPlanResponse) GetOrganization() *Organization { + if x != nil { + return x.Organization + } + return nil +} + +func (x *AdminSetPlanResponse) GetPlanId() string { + if x != nil { + return x.PlanId + } + return "" +} + var File_obiente_cloud_organizations_v1_organization_service_proto protoreflect.FileDescriptor const file_obiente_cloud_organizations_v1_organization_service_proto_rawDesc = "" + @@ -3078,8 +3184,15 @@ const file_obiente_cloud_organizations_v1_organization_service_proto_rawDesc = " "\x17GetMyPermissionsRequest\x12'\n" + "\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\"<\n" + "\x18GetMyPermissionsResponse\x12 \n" + - "\vpermissions\x18\x01 \x03(\tR\vpermissions2\xa1\x13\n" + - "\x13OrganizationService\x12\x88\x01\n" + + "\vpermissions\x18\x01 \x03(\tR\vpermissions\"W\n" + + "\x13AdminSetPlanRequest\x12'\n" + + "\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\x12\x17\n" + + "\aplan_id\x18\x02 \x01(\tR\x06planId\"\x81\x01\n" + + "\x14AdminSetPlanResponse\x12P\n" + + "\forganization\x18\x01 \x01(\v2,.obiente.cloud.organizations.v1.OrganizationR\forganization\x12\x17\n" + + "\aplan_id\x18\x02 \x01(\tR\x06planId2\x9c\x14\n" + + "\x13OrganizationService\x12y\n" + + "\fAdminSetPlan\x123.obiente.cloud.organizations.v1.AdminSetPlanRequest\x1a4.obiente.cloud.organizations.v1.AdminSetPlanResponse\x12\x88\x01\n" + "\x11ListOrganizations\x128.obiente.cloud.organizations.v1.ListOrganizationsRequest\x1a9.obiente.cloud.organizations.v1.ListOrganizationsResponse\x12\x8b\x01\n" + "\x12CreateOrganization\x129.obiente.cloud.organizations.v1.CreateOrganizationRequest\x1a:.obiente.cloud.organizations.v1.CreateOrganizationResponse\x12\x82\x01\n" + "\x0fGetOrganization\x126.obiente.cloud.organizations.v1.GetOrganizationRequest\x1a7.obiente.cloud.organizations.v1.GetOrganizationResponse\x12\x8b\x01\n" + @@ -3113,7 +3226,7 @@ func file_obiente_cloud_organizations_v1_organization_service_proto_rawDescGZIP( return file_obiente_cloud_organizations_v1_organization_service_proto_rawDescData } -var file_obiente_cloud_organizations_v1_organization_service_proto_msgTypes = make([]protoimpl.MessageInfo, 45) +var file_obiente_cloud_organizations_v1_organization_service_proto_msgTypes = make([]protoimpl.MessageInfo, 47) var file_obiente_cloud_organizations_v1_organization_service_proto_goTypes = []any{ (*GetUsageRequest)(nil), // 0: obiente.cloud.organizations.v1.GetUsageRequest (*GetUsageResponse)(nil), // 1: obiente.cloud.organizations.v1.GetUsageResponse @@ -3160,81 +3273,86 @@ var file_obiente_cloud_organizations_v1_organization_service_proto_goTypes = []a (*CreditTransaction)(nil), // 42: obiente.cloud.organizations.v1.CreditTransaction (*GetMyPermissionsRequest)(nil), // 43: obiente.cloud.organizations.v1.GetMyPermissionsRequest (*GetMyPermissionsResponse)(nil), // 44: obiente.cloud.organizations.v1.GetMyPermissionsResponse - (*v1.Pagination)(nil), // 45: obiente.cloud.common.v1.Pagination - (*timestamppb.Timestamp)(nil), // 46: google.protobuf.Timestamp - (*v11.User)(nil), // 47: obiente.cloud.auth.v1.User + (*AdminSetPlanRequest)(nil), // 45: obiente.cloud.organizations.v1.AdminSetPlanRequest + (*AdminSetPlanResponse)(nil), // 46: obiente.cloud.organizations.v1.AdminSetPlanResponse + (*v1.Pagination)(nil), // 47: obiente.cloud.common.v1.Pagination + (*timestamppb.Timestamp)(nil), // 48: google.protobuf.Timestamp + (*v11.User)(nil), // 49: obiente.cloud.auth.v1.User } var file_obiente_cloud_organizations_v1_organization_service_proto_depIdxs = []int32{ 2, // 0: obiente.cloud.organizations.v1.GetUsageResponse.current:type_name -> obiente.cloud.organizations.v1.UsageMetrics 2, // 1: obiente.cloud.organizations.v1.GetUsageResponse.estimated_monthly:type_name -> obiente.cloud.organizations.v1.UsageMetrics 3, // 2: obiente.cloud.organizations.v1.GetUsageResponse.quota:type_name -> obiente.cloud.organizations.v1.UsageQuota 31, // 3: obiente.cloud.organizations.v1.ListOrganizationsResponse.organizations:type_name -> obiente.cloud.organizations.v1.Organization - 45, // 4: obiente.cloud.organizations.v1.ListOrganizationsResponse.pagination:type_name -> obiente.cloud.common.v1.Pagination + 47, // 4: obiente.cloud.organizations.v1.ListOrganizationsResponse.pagination:type_name -> obiente.cloud.common.v1.Pagination 31, // 5: obiente.cloud.organizations.v1.CreateOrganizationResponse.organization:type_name -> obiente.cloud.organizations.v1.Organization 31, // 6: obiente.cloud.organizations.v1.GetOrganizationResponse.organization:type_name -> obiente.cloud.organizations.v1.Organization 31, // 7: obiente.cloud.organizations.v1.UpdateOrganizationResponse.organization:type_name -> obiente.cloud.organizations.v1.Organization 33, // 8: obiente.cloud.organizations.v1.ListMembersResponse.members:type_name -> obiente.cloud.organizations.v1.OrganizationMember - 45, // 9: obiente.cloud.organizations.v1.ListMembersResponse.pagination:type_name -> obiente.cloud.common.v1.Pagination + 47, // 9: obiente.cloud.organizations.v1.ListMembersResponse.pagination:type_name -> obiente.cloud.common.v1.Pagination 33, // 10: obiente.cloud.organizations.v1.InviteMemberResponse.member:type_name -> obiente.cloud.organizations.v1.OrganizationMember 20, // 11: obiente.cloud.organizations.v1.ListMyInvitesResponse.invites:type_name -> obiente.cloud.organizations.v1.PendingInvite - 45, // 12: obiente.cloud.organizations.v1.ListMyInvitesResponse.pagination:type_name -> obiente.cloud.common.v1.Pagination - 46, // 13: obiente.cloud.organizations.v1.PendingInvite.invited_at:type_name -> google.protobuf.Timestamp + 47, // 12: obiente.cloud.organizations.v1.ListMyInvitesResponse.pagination:type_name -> obiente.cloud.common.v1.Pagination + 48, // 13: obiente.cloud.organizations.v1.PendingInvite.invited_at:type_name -> google.protobuf.Timestamp 33, // 14: obiente.cloud.organizations.v1.AcceptInviteResponse.member:type_name -> obiente.cloud.organizations.v1.OrganizationMember 31, // 15: obiente.cloud.organizations.v1.AcceptInviteResponse.organization:type_name -> obiente.cloud.organizations.v1.Organization 33, // 16: obiente.cloud.organizations.v1.UpdateMemberResponse.member:type_name -> obiente.cloud.organizations.v1.OrganizationMember - 46, // 17: obiente.cloud.organizations.v1.Organization.created_at:type_name -> google.protobuf.Timestamp + 48, // 17: obiente.cloud.organizations.v1.Organization.created_at:type_name -> google.protobuf.Timestamp 32, // 18: obiente.cloud.organizations.v1.Organization.plan_info:type_name -> obiente.cloud.organizations.v1.PlanInfo - 47, // 19: obiente.cloud.organizations.v1.OrganizationMember.user:type_name -> obiente.cloud.auth.v1.User - 46, // 20: obiente.cloud.organizations.v1.OrganizationMember.joined_at:type_name -> google.protobuf.Timestamp + 49, // 19: obiente.cloud.organizations.v1.OrganizationMember.user:type_name -> obiente.cloud.auth.v1.User + 48, // 20: obiente.cloud.organizations.v1.OrganizationMember.joined_at:type_name -> google.protobuf.Timestamp 31, // 21: obiente.cloud.organizations.v1.AddCreditsResponse.organization:type_name -> obiente.cloud.organizations.v1.Organization 31, // 22: obiente.cloud.organizations.v1.AdminAddCreditsResponse.organization:type_name -> obiente.cloud.organizations.v1.Organization 31, // 23: obiente.cloud.organizations.v1.AdminRemoveCreditsResponse.organization:type_name -> obiente.cloud.organizations.v1.Organization 42, // 24: obiente.cloud.organizations.v1.GetCreditLogResponse.transactions:type_name -> obiente.cloud.organizations.v1.CreditTransaction - 45, // 25: obiente.cloud.organizations.v1.GetCreditLogResponse.pagination:type_name -> obiente.cloud.common.v1.Pagination - 46, // 26: obiente.cloud.organizations.v1.CreditTransaction.created_at:type_name -> google.protobuf.Timestamp - 4, // 27: obiente.cloud.organizations.v1.OrganizationService.ListOrganizations:input_type -> obiente.cloud.organizations.v1.ListOrganizationsRequest - 6, // 28: obiente.cloud.organizations.v1.OrganizationService.CreateOrganization:input_type -> obiente.cloud.organizations.v1.CreateOrganizationRequest - 8, // 29: obiente.cloud.organizations.v1.OrganizationService.GetOrganization:input_type -> obiente.cloud.organizations.v1.GetOrganizationRequest - 10, // 30: obiente.cloud.organizations.v1.OrganizationService.UpdateOrganization:input_type -> obiente.cloud.organizations.v1.UpdateOrganizationRequest - 12, // 31: obiente.cloud.organizations.v1.OrganizationService.ListMembers:input_type -> obiente.cloud.organizations.v1.ListMembersRequest - 14, // 32: obiente.cloud.organizations.v1.OrganizationService.InviteMember:input_type -> obiente.cloud.organizations.v1.InviteMemberRequest - 16, // 33: obiente.cloud.organizations.v1.OrganizationService.ResendInvite:input_type -> obiente.cloud.organizations.v1.ResendInviteRequest - 18, // 34: obiente.cloud.organizations.v1.OrganizationService.ListMyInvites:input_type -> obiente.cloud.organizations.v1.ListMyInvitesRequest - 21, // 35: obiente.cloud.organizations.v1.OrganizationService.AcceptInvite:input_type -> obiente.cloud.organizations.v1.AcceptInviteRequest - 23, // 36: obiente.cloud.organizations.v1.OrganizationService.DeclineInvite:input_type -> obiente.cloud.organizations.v1.DeclineInviteRequest - 25, // 37: obiente.cloud.organizations.v1.OrganizationService.UpdateMember:input_type -> obiente.cloud.organizations.v1.UpdateMemberRequest - 27, // 38: obiente.cloud.organizations.v1.OrganizationService.RemoveMember:input_type -> obiente.cloud.organizations.v1.RemoveMemberRequest - 29, // 39: obiente.cloud.organizations.v1.OrganizationService.TransferOwnership:input_type -> obiente.cloud.organizations.v1.TransferOwnershipRequest - 0, // 40: obiente.cloud.organizations.v1.OrganizationService.GetUsage:input_type -> obiente.cloud.organizations.v1.GetUsageRequest - 34, // 41: obiente.cloud.organizations.v1.OrganizationService.AddCredits:input_type -> obiente.cloud.organizations.v1.AddCreditsRequest - 36, // 42: obiente.cloud.organizations.v1.OrganizationService.AdminAddCredits:input_type -> obiente.cloud.organizations.v1.AdminAddCreditsRequest - 38, // 43: obiente.cloud.organizations.v1.OrganizationService.AdminRemoveCredits:input_type -> obiente.cloud.organizations.v1.AdminRemoveCreditsRequest - 40, // 44: obiente.cloud.organizations.v1.OrganizationService.GetCreditLog:input_type -> obiente.cloud.organizations.v1.GetCreditLogRequest - 43, // 45: obiente.cloud.organizations.v1.OrganizationService.GetMyPermissions:input_type -> obiente.cloud.organizations.v1.GetMyPermissionsRequest - 5, // 46: obiente.cloud.organizations.v1.OrganizationService.ListOrganizations:output_type -> obiente.cloud.organizations.v1.ListOrganizationsResponse - 7, // 47: obiente.cloud.organizations.v1.OrganizationService.CreateOrganization:output_type -> obiente.cloud.organizations.v1.CreateOrganizationResponse - 9, // 48: obiente.cloud.organizations.v1.OrganizationService.GetOrganization:output_type -> obiente.cloud.organizations.v1.GetOrganizationResponse - 11, // 49: obiente.cloud.organizations.v1.OrganizationService.UpdateOrganization:output_type -> obiente.cloud.organizations.v1.UpdateOrganizationResponse - 13, // 50: obiente.cloud.organizations.v1.OrganizationService.ListMembers:output_type -> obiente.cloud.organizations.v1.ListMembersResponse - 15, // 51: obiente.cloud.organizations.v1.OrganizationService.InviteMember:output_type -> obiente.cloud.organizations.v1.InviteMemberResponse - 17, // 52: obiente.cloud.organizations.v1.OrganizationService.ResendInvite:output_type -> obiente.cloud.organizations.v1.ResendInviteResponse - 19, // 53: obiente.cloud.organizations.v1.OrganizationService.ListMyInvites:output_type -> obiente.cloud.organizations.v1.ListMyInvitesResponse - 22, // 54: obiente.cloud.organizations.v1.OrganizationService.AcceptInvite:output_type -> obiente.cloud.organizations.v1.AcceptInviteResponse - 24, // 55: obiente.cloud.organizations.v1.OrganizationService.DeclineInvite:output_type -> obiente.cloud.organizations.v1.DeclineInviteResponse - 26, // 56: obiente.cloud.organizations.v1.OrganizationService.UpdateMember:output_type -> obiente.cloud.organizations.v1.UpdateMemberResponse - 28, // 57: obiente.cloud.organizations.v1.OrganizationService.RemoveMember:output_type -> obiente.cloud.organizations.v1.RemoveMemberResponse - 30, // 58: obiente.cloud.organizations.v1.OrganizationService.TransferOwnership:output_type -> obiente.cloud.organizations.v1.TransferOwnershipResponse - 1, // 59: obiente.cloud.organizations.v1.OrganizationService.GetUsage:output_type -> obiente.cloud.organizations.v1.GetUsageResponse - 35, // 60: obiente.cloud.organizations.v1.OrganizationService.AddCredits:output_type -> obiente.cloud.organizations.v1.AddCreditsResponse - 37, // 61: obiente.cloud.organizations.v1.OrganizationService.AdminAddCredits:output_type -> obiente.cloud.organizations.v1.AdminAddCreditsResponse - 39, // 62: obiente.cloud.organizations.v1.OrganizationService.AdminRemoveCredits:output_type -> obiente.cloud.organizations.v1.AdminRemoveCreditsResponse - 41, // 63: obiente.cloud.organizations.v1.OrganizationService.GetCreditLog:output_type -> obiente.cloud.organizations.v1.GetCreditLogResponse - 44, // 64: obiente.cloud.organizations.v1.OrganizationService.GetMyPermissions:output_type -> obiente.cloud.organizations.v1.GetMyPermissionsResponse - 46, // [46:65] is the sub-list for method output_type - 27, // [27:46] is the sub-list for method input_type - 27, // [27:27] is the sub-list for extension type_name - 27, // [27:27] is the sub-list for extension extendee - 0, // [0:27] is the sub-list for field type_name + 47, // 25: obiente.cloud.organizations.v1.GetCreditLogResponse.pagination:type_name -> obiente.cloud.common.v1.Pagination + 48, // 26: obiente.cloud.organizations.v1.CreditTransaction.created_at:type_name -> google.protobuf.Timestamp + 31, // 27: obiente.cloud.organizations.v1.AdminSetPlanResponse.organization:type_name -> obiente.cloud.organizations.v1.Organization + 45, // 28: obiente.cloud.organizations.v1.OrganizationService.AdminSetPlan:input_type -> obiente.cloud.organizations.v1.AdminSetPlanRequest + 4, // 29: obiente.cloud.organizations.v1.OrganizationService.ListOrganizations:input_type -> obiente.cloud.organizations.v1.ListOrganizationsRequest + 6, // 30: obiente.cloud.organizations.v1.OrganizationService.CreateOrganization:input_type -> obiente.cloud.organizations.v1.CreateOrganizationRequest + 8, // 31: obiente.cloud.organizations.v1.OrganizationService.GetOrganization:input_type -> obiente.cloud.organizations.v1.GetOrganizationRequest + 10, // 32: obiente.cloud.organizations.v1.OrganizationService.UpdateOrganization:input_type -> obiente.cloud.organizations.v1.UpdateOrganizationRequest + 12, // 33: obiente.cloud.organizations.v1.OrganizationService.ListMembers:input_type -> obiente.cloud.organizations.v1.ListMembersRequest + 14, // 34: obiente.cloud.organizations.v1.OrganizationService.InviteMember:input_type -> obiente.cloud.organizations.v1.InviteMemberRequest + 16, // 35: obiente.cloud.organizations.v1.OrganizationService.ResendInvite:input_type -> obiente.cloud.organizations.v1.ResendInviteRequest + 18, // 36: obiente.cloud.organizations.v1.OrganizationService.ListMyInvites:input_type -> obiente.cloud.organizations.v1.ListMyInvitesRequest + 21, // 37: obiente.cloud.organizations.v1.OrganizationService.AcceptInvite:input_type -> obiente.cloud.organizations.v1.AcceptInviteRequest + 23, // 38: obiente.cloud.organizations.v1.OrganizationService.DeclineInvite:input_type -> obiente.cloud.organizations.v1.DeclineInviteRequest + 25, // 39: obiente.cloud.organizations.v1.OrganizationService.UpdateMember:input_type -> obiente.cloud.organizations.v1.UpdateMemberRequest + 27, // 40: obiente.cloud.organizations.v1.OrganizationService.RemoveMember:input_type -> obiente.cloud.organizations.v1.RemoveMemberRequest + 29, // 41: obiente.cloud.organizations.v1.OrganizationService.TransferOwnership:input_type -> obiente.cloud.organizations.v1.TransferOwnershipRequest + 0, // 42: obiente.cloud.organizations.v1.OrganizationService.GetUsage:input_type -> obiente.cloud.organizations.v1.GetUsageRequest + 34, // 43: obiente.cloud.organizations.v1.OrganizationService.AddCredits:input_type -> obiente.cloud.organizations.v1.AddCreditsRequest + 36, // 44: obiente.cloud.organizations.v1.OrganizationService.AdminAddCredits:input_type -> obiente.cloud.organizations.v1.AdminAddCreditsRequest + 38, // 45: obiente.cloud.organizations.v1.OrganizationService.AdminRemoveCredits:input_type -> obiente.cloud.organizations.v1.AdminRemoveCreditsRequest + 40, // 46: obiente.cloud.organizations.v1.OrganizationService.GetCreditLog:input_type -> obiente.cloud.organizations.v1.GetCreditLogRequest + 43, // 47: obiente.cloud.organizations.v1.OrganizationService.GetMyPermissions:input_type -> obiente.cloud.organizations.v1.GetMyPermissionsRequest + 46, // 48: obiente.cloud.organizations.v1.OrganizationService.AdminSetPlan:output_type -> obiente.cloud.organizations.v1.AdminSetPlanResponse + 5, // 49: obiente.cloud.organizations.v1.OrganizationService.ListOrganizations:output_type -> obiente.cloud.organizations.v1.ListOrganizationsResponse + 7, // 50: obiente.cloud.organizations.v1.OrganizationService.CreateOrganization:output_type -> obiente.cloud.organizations.v1.CreateOrganizationResponse + 9, // 51: obiente.cloud.organizations.v1.OrganizationService.GetOrganization:output_type -> obiente.cloud.organizations.v1.GetOrganizationResponse + 11, // 52: obiente.cloud.organizations.v1.OrganizationService.UpdateOrganization:output_type -> obiente.cloud.organizations.v1.UpdateOrganizationResponse + 13, // 53: obiente.cloud.organizations.v1.OrganizationService.ListMembers:output_type -> obiente.cloud.organizations.v1.ListMembersResponse + 15, // 54: obiente.cloud.organizations.v1.OrganizationService.InviteMember:output_type -> obiente.cloud.organizations.v1.InviteMemberResponse + 17, // 55: obiente.cloud.organizations.v1.OrganizationService.ResendInvite:output_type -> obiente.cloud.organizations.v1.ResendInviteResponse + 19, // 56: obiente.cloud.organizations.v1.OrganizationService.ListMyInvites:output_type -> obiente.cloud.organizations.v1.ListMyInvitesResponse + 22, // 57: obiente.cloud.organizations.v1.OrganizationService.AcceptInvite:output_type -> obiente.cloud.organizations.v1.AcceptInviteResponse + 24, // 58: obiente.cloud.organizations.v1.OrganizationService.DeclineInvite:output_type -> obiente.cloud.organizations.v1.DeclineInviteResponse + 26, // 59: obiente.cloud.organizations.v1.OrganizationService.UpdateMember:output_type -> obiente.cloud.organizations.v1.UpdateMemberResponse + 28, // 60: obiente.cloud.organizations.v1.OrganizationService.RemoveMember:output_type -> obiente.cloud.organizations.v1.RemoveMemberResponse + 30, // 61: obiente.cloud.organizations.v1.OrganizationService.TransferOwnership:output_type -> obiente.cloud.organizations.v1.TransferOwnershipResponse + 1, // 62: obiente.cloud.organizations.v1.OrganizationService.GetUsage:output_type -> obiente.cloud.organizations.v1.GetUsageResponse + 35, // 63: obiente.cloud.organizations.v1.OrganizationService.AddCredits:output_type -> obiente.cloud.organizations.v1.AddCreditsResponse + 37, // 64: obiente.cloud.organizations.v1.OrganizationService.AdminAddCredits:output_type -> obiente.cloud.organizations.v1.AdminAddCreditsResponse + 39, // 65: obiente.cloud.organizations.v1.OrganizationService.AdminRemoveCredits:output_type -> obiente.cloud.organizations.v1.AdminRemoveCreditsResponse + 41, // 66: obiente.cloud.organizations.v1.OrganizationService.GetCreditLog:output_type -> obiente.cloud.organizations.v1.GetCreditLogResponse + 44, // 67: obiente.cloud.organizations.v1.OrganizationService.GetMyPermissions:output_type -> obiente.cloud.organizations.v1.GetMyPermissionsResponse + 48, // [48:68] is the sub-list for method output_type + 28, // [28:48] is the sub-list for method input_type + 28, // [28:28] is the sub-list for extension type_name + 28, // [28:28] is the sub-list for extension extendee + 0, // [0:28] is the sub-list for field type_name } func init() { file_obiente_cloud_organizations_v1_organization_service_proto_init() } @@ -3258,7 +3376,7 @@ func file_obiente_cloud_organizations_v1_organization_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_obiente_cloud_organizations_v1_organization_service_proto_rawDesc), len(file_obiente_cloud_organizations_v1_organization_service_proto_rawDesc)), NumEnums: 0, - NumMessages: 45, + NumMessages: 47, NumExtensions: 0, NumServices: 1, }, diff --git a/apps/shared/proto/obiente/cloud/organizations/v1/organizationsv1connect/organization_service.connect.go b/apps/shared/proto/obiente/cloud/organizations/v1/organizationsv1connect/organization_service.connect.go index 715d2425..ee9d4c45 100644 --- a/apps/shared/proto/obiente/cloud/organizations/v1/organizationsv1connect/organization_service.connect.go +++ b/apps/shared/proto/obiente/cloud/organizations/v1/organizationsv1connect/organization_service.connect.go @@ -33,6 +33,9 @@ const ( // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( + // OrganizationServiceAdminSetPlanProcedure is the fully-qualified name of the OrganizationService's + // AdminSetPlan RPC. + OrganizationServiceAdminSetPlanProcedure = "/obiente.cloud.organizations.v1.OrganizationService/AdminSetPlan" // OrganizationServiceListOrganizationsProcedure is the fully-qualified name of the // OrganizationService's ListOrganizations RPC. OrganizationServiceListOrganizationsProcedure = "/obiente.cloud.organizations.v1.OrganizationService/ListOrganizations" @@ -95,6 +98,8 @@ const ( // OrganizationServiceClient is a client for the obiente.cloud.organizations.v1.OrganizationService // service. type OrganizationServiceClient interface { + // Admin: Set the active plan for an organization (superadmin only) + AdminSetPlan(context.Context, *connect.Request[v1.AdminSetPlanRequest]) (*connect.Response[v1.AdminSetPlanResponse], error) // List user's organizations ListOrganizations(context.Context, *connect.Request[v1.ListOrganizationsRequest]) (*connect.Response[v1.ListOrganizationsResponse], error) // Create new organization @@ -147,6 +152,12 @@ func NewOrganizationServiceClient(httpClient connect.HTTPClient, baseURL string, baseURL = strings.TrimRight(baseURL, "/") organizationServiceMethods := v1.File_obiente_cloud_organizations_v1_organization_service_proto.Services().ByName("OrganizationService").Methods() return &organizationServiceClient{ + adminSetPlan: connect.NewClient[v1.AdminSetPlanRequest, v1.AdminSetPlanResponse]( + httpClient, + baseURL+OrganizationServiceAdminSetPlanProcedure, + connect.WithSchema(organizationServiceMethods.ByName("AdminSetPlan")), + connect.WithClientOptions(opts...), + ), listOrganizations: connect.NewClient[v1.ListOrganizationsRequest, v1.ListOrganizationsResponse]( httpClient, baseURL+OrganizationServiceListOrganizationsProcedure, @@ -266,6 +277,7 @@ func NewOrganizationServiceClient(httpClient connect.HTTPClient, baseURL string, // organizationServiceClient implements OrganizationServiceClient. type organizationServiceClient struct { + adminSetPlan *connect.Client[v1.AdminSetPlanRequest, v1.AdminSetPlanResponse] listOrganizations *connect.Client[v1.ListOrganizationsRequest, v1.ListOrganizationsResponse] createOrganization *connect.Client[v1.CreateOrganizationRequest, v1.CreateOrganizationResponse] getOrganization *connect.Client[v1.GetOrganizationRequest, v1.GetOrganizationResponse] @@ -287,6 +299,11 @@ type organizationServiceClient struct { getMyPermissions *connect.Client[v1.GetMyPermissionsRequest, v1.GetMyPermissionsResponse] } +// AdminSetPlan calls obiente.cloud.organizations.v1.OrganizationService.AdminSetPlan. +func (c *organizationServiceClient) AdminSetPlan(ctx context.Context, req *connect.Request[v1.AdminSetPlanRequest]) (*connect.Response[v1.AdminSetPlanResponse], error) { + return c.adminSetPlan.CallUnary(ctx, req) +} + // ListOrganizations calls obiente.cloud.organizations.v1.OrganizationService.ListOrganizations. func (c *organizationServiceClient) ListOrganizations(ctx context.Context, req *connect.Request[v1.ListOrganizationsRequest]) (*connect.Response[v1.ListOrganizationsResponse], error) { return c.listOrganizations.CallUnary(ctx, req) @@ -385,6 +402,8 @@ func (c *organizationServiceClient) GetMyPermissions(ctx context.Context, req *c // OrganizationServiceHandler is an implementation of the // obiente.cloud.organizations.v1.OrganizationService service. type OrganizationServiceHandler interface { + // Admin: Set the active plan for an organization (superadmin only) + AdminSetPlan(context.Context, *connect.Request[v1.AdminSetPlanRequest]) (*connect.Response[v1.AdminSetPlanResponse], error) // List user's organizations ListOrganizations(context.Context, *connect.Request[v1.ListOrganizationsRequest]) (*connect.Response[v1.ListOrganizationsResponse], error) // Create new organization @@ -432,6 +451,12 @@ type OrganizationServiceHandler interface { // and JSON codecs. They also support gzip compression. func NewOrganizationServiceHandler(svc OrganizationServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { organizationServiceMethods := v1.File_obiente_cloud_organizations_v1_organization_service_proto.Services().ByName("OrganizationService").Methods() + organizationServiceAdminSetPlanHandler := connect.NewUnaryHandler( + OrganizationServiceAdminSetPlanProcedure, + svc.AdminSetPlan, + connect.WithSchema(organizationServiceMethods.ByName("AdminSetPlan")), + connect.WithHandlerOptions(opts...), + ) organizationServiceListOrganizationsHandler := connect.NewUnaryHandler( OrganizationServiceListOrganizationsProcedure, svc.ListOrganizations, @@ -548,6 +573,8 @@ func NewOrganizationServiceHandler(svc OrganizationServiceHandler, opts ...conne ) return "/obiente.cloud.organizations.v1.OrganizationService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { + case OrganizationServiceAdminSetPlanProcedure: + organizationServiceAdminSetPlanHandler.ServeHTTP(w, r) case OrganizationServiceListOrganizationsProcedure: organizationServiceListOrganizationsHandler.ServeHTTP(w, r) case OrganizationServiceCreateOrganizationProcedure: @@ -595,6 +622,10 @@ func NewOrganizationServiceHandler(svc OrganizationServiceHandler, opts ...conne // UnimplementedOrganizationServiceHandler returns CodeUnimplemented from all methods. type UnimplementedOrganizationServiceHandler struct{} +func (UnimplementedOrganizationServiceHandler) AdminSetPlan(context.Context, *connect.Request[v1.AdminSetPlanRequest]) (*connect.Response[v1.AdminSetPlanResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("obiente.cloud.organizations.v1.OrganizationService.AdminSetPlan is not implemented")) +} + func (UnimplementedOrganizationServiceHandler) ListOrganizations(context.Context, *connect.Request[v1.ListOrganizationsRequest]) (*connect.Response[v1.ListOrganizationsResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("obiente.cloud.organizations.v1.OrganizationService.ListOrganizations is not implemented")) } diff --git a/packages/proto/proto/obiente/cloud/organizations/v1/organization_service.proto b/packages/proto/proto/obiente/cloud/organizations/v1/organization_service.proto index 06507409..6e013484 100644 --- a/packages/proto/proto/obiente/cloud/organizations/v1/organization_service.proto +++ b/packages/proto/proto/obiente/cloud/organizations/v1/organization_service.proto @@ -2,46 +2,49 @@ syntax = "proto3"; package obiente.cloud.organizations.v1; -option go_package = "github.com/obiente/cloud/apps/shared/proto/obiente/cloud/organizations/v1;organizationsv1"; - import "google/protobuf/timestamp.proto"; import "obiente/cloud/auth/v1/auth_service.proto"; import "obiente/cloud/common/v1/common.proto"; +option go_package = "github.com/obiente/cloud/apps/shared/proto/obiente/cloud/organizations/v1;organizationsv1"; + service OrganizationService { + // Admin: Set the active plan for an organization (superadmin only) + rpc AdminSetPlan(AdminSetPlanRequest) returns (AdminSetPlanResponse); + // List user's organizations rpc ListOrganizations(ListOrganizationsRequest) returns (ListOrganizationsResponse); - + // Create new organization rpc CreateOrganization(CreateOrganizationRequest) returns (CreateOrganizationResponse); - + // Get organization details rpc GetOrganization(GetOrganizationRequest) returns (GetOrganizationResponse); - + // Update organization rpc UpdateOrganization(UpdateOrganizationRequest) returns (UpdateOrganizationResponse); - + // List organization members rpc ListMembers(ListMembersRequest) returns (ListMembersResponse); - + // Invite user to organization rpc InviteMember(InviteMemberRequest) returns (InviteMemberResponse); - + // Resend invitation email to a pending member rpc ResendInvite(ResendInviteRequest) returns (ResendInviteResponse); - + // List invites sent to the current user rpc ListMyInvites(ListMyInvitesRequest) returns (ListMyInvitesResponse); - + // Accept an invitation to join an organization rpc AcceptInvite(AcceptInviteRequest) returns (AcceptInviteResponse); - + // Decline an invitation to join an organization rpc DeclineInvite(DeclineInviteRequest) returns (DeclineInviteResponse); - + // Update member role/permissions rpc UpdateMember(UpdateMemberRequest) returns (UpdateMemberResponse); - + // Remove member from organization rpc RemoveMember(RemoveMemberRequest) returns (RemoveMemberResponse); @@ -297,7 +300,6 @@ message OrganizationMember { google.protobuf.Timestamp joined_at = 5; } - message AddCreditsRequest { string organization_id = 1; // Amount in cents ($0.01 units). Must be positive. @@ -382,4 +384,16 @@ message GetMyPermissionsRequest { message GetMyPermissionsResponse { repeated string permissions = 1; -} \ No newline at end of file +} + +// Request to set the active plan for an organization (superadmin only) +message AdminSetPlanRequest { + string organization_id = 1; + string plan_id = 2; // The plan to assign (must match OrganizationPlan.ID) +} + +// Response for AdminSetPlan +message AdminSetPlanResponse { + Organization organization = 1; + string plan_id = 2; +} diff --git a/packages/proto/src/generated/obiente/cloud/deployments/v1/deployment_service_pb.ts b/packages/proto/src/generated/obiente/cloud/deployments/v1/deployment_service_pb.ts index e3262c86..2f587cdb 100644 --- a/packages/proto/src/generated/obiente/cloud/deployments/v1/deployment_service_pb.ts +++ b/packages/proto/src/generated/obiente/cloud/deployments/v1/deployment_service_pb.ts @@ -4622,7 +4622,7 @@ export const BuildStatusSchema: GenEnum = /*@__PURE__*/ */ export enum HealthCheckType { /** - * No health check + * Auto-detect (TCP if routing exists, otherwise no healthcheck) * * @generated from enum value: HEALTHCHECK_TYPE_UNSPECIFIED = 0; */ diff --git a/packages/proto/src/generated/obiente/cloud/organizations/v1/organization_service_pb.ts b/packages/proto/src/generated/obiente/cloud/organizations/v1/organization_service_pb.ts index 0af4b1ba..078a9321 100644 --- a/packages/proto/src/generated/obiente/cloud/organizations/v1/organization_service_pb.ts +++ b/packages/proto/src/generated/obiente/cloud/organizations/v1/organization_service_pb.ts @@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file obiente/cloud/organizations/v1/organization_service.proto. */ export const file_obiente_cloud_organizations_v1_organization_service: GenFile = /*@__PURE__*/ - fileDesc("CjlvYmllbnRlL2Nsb3VkL29yZ2FuaXphdGlvbnMvdjEvb3JnYW5pemF0aW9uX3NlcnZpY2UucHJvdG8SHm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MSJICg9HZXRVc2FnZVJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhIKBW1vbnRoGAIgASgJSACIAQFCCAoGX21vbnRoIv0BChBHZXRVc2FnZVJlc3BvbnNlEhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRINCgVtb250aBgCIAEoCRI9CgdjdXJyZW50GAMgASgLMiwub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLlVzYWdlTWV0cmljcxJHChFlc3RpbWF0ZWRfbW9udGhseRgEIAEoCzIsLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Vc2FnZU1ldHJpY3MSOQoFcXVvdGEYBSABKAsyKi5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuVXNhZ2VRdW90YSLpAwoMVXNhZ2VNZXRyaWNzEhgKEGNwdV9jb3JlX3NlY29uZHMYASABKAMSGwoTbWVtb3J5X2J5dGVfc2Vjb25kcxgCIAEoAxIaChJiYW5kd2lkdGhfcnhfYnl0ZXMYAyABKAMSGgoSYmFuZHdpZHRoX3R4X2J5dGVzGAQgASgDEhUKDXN0b3JhZ2VfYnl0ZXMYBSABKAMSHwoXZGVwbG95bWVudHNfYWN0aXZlX3BlYWsYBiABKAUSHAoUZXN0aW1hdGVkX2Nvc3RfY2VudHMYByABKAMSGwoOY3B1X2Nvc3RfY2VudHMYCCABKANIAIgBARIeChFtZW1vcnlfY29zdF9jZW50cxgJIAEoA0gBiAEBEiEKFGJhbmR3aWR0aF9jb3N0X2NlbnRzGAogASgDSAKIAQESHwoSc3RvcmFnZV9jb3N0X2NlbnRzGAsgASgDSAOIAQESIQoUcHVibGljX2lwX2Nvc3RfY2VudHMYDCABKANIBIgBAUIRCg9fY3B1X2Nvc3RfY2VudHNCFAoSX21lbW9yeV9jb3N0X2NlbnRzQhcKFV9iYW5kd2lkdGhfY29zdF9jZW50c0IVChNfc3RvcmFnZV9jb3N0X2NlbnRzQhcKFV9wdWJsaWNfaXBfY29zdF9jZW50cyKkAQoKVXNhZ2VRdW90YRIgChhjcHVfY29yZV9zZWNvbmRzX21vbnRobHkYASABKAMSIwobbWVtb3J5X2J5dGVfc2Vjb25kc19tb250aGx5GAIgASgDEh8KF2JhbmR3aWR0aF9ieXRlc19tb250aGx5GAMgASgDEhUKDXN0b3JhZ2VfYnl0ZXMYBCABKAMSFwoPZGVwbG95bWVudHNfbWF4GAUgASgFImAKGExpc3RPcmdhbml6YXRpb25zUmVxdWVzdBIMCgRwYWdlGAEgASgFEhAKCHBlcl9wYWdlGAIgASgFEhYKCW9ubHlfbWluZRgDIAEoCEgAiAEBQgwKCl9vbmx5X21pbmUimQEKGUxpc3RPcmdhbml6YXRpb25zUmVzcG9uc2USQwoNb3JnYW5pemF0aW9ucxgBIAMoCzIsLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Pcmdhbml6YXRpb24SNwoKcGFnaW5hdGlvbhgCIAEoCzIjLm9iaWVudGUuY2xvdWQuY29tbW9uLnYxLlBhZ2luYXRpb24iRQoZQ3JlYXRlT3JnYW5pemF0aW9uUmVxdWVzdBIMCgRuYW1lGAEgASgJEgwKBHNsdWcYAiABKAkSDAoEcGxhbhgDIAEoCSJgChpDcmVhdGVPcmdhbml6YXRpb25SZXNwb25zZRJCCgxvcmdhbml6YXRpb24YASABKAsyLC5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuT3JnYW5pemF0aW9uIjEKFkdldE9yZ2FuaXphdGlvblJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJIl0KF0dldE9yZ2FuaXphdGlvblJlc3BvbnNlEkIKDG9yZ2FuaXphdGlvbhgBIAEoCzIsLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Pcmdhbml6YXRpb24icAoZVXBkYXRlT3JnYW5pemF0aW9uUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSEQoEbmFtZRgCIAEoCUgAiAEBEhMKBmRvbWFpbhgDIAEoCUgBiAEBQgcKBV9uYW1lQgkKB19kb21haW4iYAoaVXBkYXRlT3JnYW5pemF0aW9uUmVzcG9uc2USQgoMb3JnYW5pemF0aW9uGAEgASgLMiwub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLk9yZ2FuaXphdGlvbiJNChJMaXN0TWVtYmVyc1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEgwKBHBhZ2UYAiABKAUSEAoIcGVyX3BhZ2UYAyABKAUikwEKE0xpc3RNZW1iZXJzUmVzcG9uc2USQwoHbWVtYmVycxgBIAMoCzIyLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Pcmdhbml6YXRpb25NZW1iZXISNwoKcGFnaW5hdGlvbhgCIAEoCzIjLm9iaWVudGUuY2xvdWQuY29tbW9uLnYxLlBhZ2luYXRpb24iSwoTSW52aXRlTWVtYmVyUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSDQoFZW1haWwYAiABKAkSDAoEcm9sZRgDIAEoCSJaChRJbnZpdGVNZW1iZXJSZXNwb25zZRJCCgZtZW1iZXIYASABKAsyMi5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuT3JnYW5pemF0aW9uTWVtYmVyIkEKE1Jlc2VuZEludml0ZVJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhEKCW1lbWJlcl9pZBgCIAEoCSInChRSZXNlbmRJbnZpdGVSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIjYKFExpc3RNeUludml0ZXNSZXF1ZXN0EgwKBHBhZ2UYASABKAUSEAoIcGVyX3BhZ2UYAiABKAUikAEKFUxpc3RNeUludml0ZXNSZXNwb25zZRI+CgdpbnZpdGVzGAEgAygLMi0ub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLlBlbmRpbmdJbnZpdGUSNwoKcGFnaW5hdGlvbhgCIAEoCzIjLm9iaWVudGUuY2xvdWQuY29tbW9uLnYxLlBhZ2luYXRpb24ipAEKDVBlbmRpbmdJbnZpdGUSCgoCaWQYASABKAkSFwoPb3JnYW5pemF0aW9uX2lkGAIgASgJEhkKEW9yZ2FuaXphdGlvbl9uYW1lGAMgASgJEgwKBHJvbGUYBCABKAkSLgoKaW52aXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFQoNaW52aXRlcl9lbWFpbBgGIAEoCSJBChNBY2NlcHRJbnZpdGVSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIRCgltZW1iZXJfaWQYAiABKAkingEKFEFjY2VwdEludml0ZVJlc3BvbnNlEkIKBm1lbWJlchgBIAEoCzIyLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Pcmdhbml6YXRpb25NZW1iZXISQgoMb3JnYW5pemF0aW9uGAIgASgLMiwub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLk9yZ2FuaXphdGlvbiJCChREZWNsaW5lSW52aXRlUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSEQoJbWVtYmVyX2lkGAIgASgJIigKFURlY2xpbmVJbnZpdGVSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIl0KE1VwZGF0ZU1lbWJlclJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhEKCW1lbWJlcl9pZBgCIAEoCRIRCgRyb2xlGAMgASgJSACIAQFCBwoFX3JvbGUiWgoUVXBkYXRlTWVtYmVyUmVzcG9uc2USQgoGbWVtYmVyGAEgASgLMjIub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLk9yZ2FuaXphdGlvbk1lbWJlciJBChNSZW1vdmVNZW1iZXJSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIRCgltZW1iZXJfaWQYAiABKAkiJwoUUmVtb3ZlTWVtYmVyUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCJnChhUcmFuc2Zlck93bmVyc2hpcFJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhsKE25ld19vd25lcl9tZW1iZXJfaWQYAiABKAkSFQoNZmFsbGJhY2tfcm9sZRgDIAEoCSKCAQoZVHJhbnNmZXJPd25lcnNoaXBSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEiAKGHByZXZpb3VzX293bmVyX21lbWJlcl9pZBgCIAEoCRIbChNuZXdfb3duZXJfbWVtYmVyX2lkGAMgASgJEhUKDWZhbGxiYWNrX3JvbGUYBCABKAki7QIKDE9yZ2FuaXphdGlvbhIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEgwKBHNsdWcYAyABKAkSEwoGZG9tYWluGAQgASgJSACIAQESDAoEcGxhbhgFIAEoCRIOCgZzdGF0dXMYBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFwoPbWF4X2RlcGxveW1lbnRzGAggASgFEhkKEW1heF92cHNfaW5zdGFuY2VzGAkgASgFEhgKEG1heF90ZWFtX21lbWJlcnMYCiABKAUSDwoHY3JlZGl0cxgLIAEoAxJACglwbGFuX2luZm8YDCABKAsyKC5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuUGxhbkluZm9IAYgBARIYChB0b3RhbF9wYWlkX2NlbnRzGA0gASgDQgkKB19kb21haW5CDAoKX3BsYW5faW5mbyKtAgoIUGxhbkluZm8SDwoHcGxhbl9pZBgBIAEoCRIRCglwbGFuX25hbWUYAiABKAkSEwoLZGVzY3JpcHRpb24YAyABKAkSEQoJY3B1X2NvcmVzGAQgASgFEhQKDG1lbW9yeV9ieXRlcxgFIAEoAxIXCg9kZXBsb3ltZW50c19tYXgYBiABKAUSGQoRbWF4X3Zwc19pbnN0YW5jZXMYCyABKAUSHQoVYmFuZHdpZHRoX2J5dGVzX21vbnRoGAcgASgDEhUKDXN0b3JhZ2VfYnl0ZXMYCCABKAMSHQoVbWluaW11bV9wYXltZW50X2NlbnRzGAkgASgDEiIKGm1vbnRobHlfZnJlZV9jcmVkaXRzX2NlbnRzGAogASgDEhIKCnRyaWFsX2RheXMYDCABKAUimAEKEk9yZ2FuaXphdGlvbk1lbWJlchIKCgJpZBgBIAEoCRIpCgR1c2VyGAIgASgLMhsub2JpZW50ZS5jbG91ZC5hdXRoLnYxLlVzZXISDAoEcm9sZRgDIAEoCRIOCgZzdGF0dXMYBCABKAkSLQoJam9pbmVkX2F0GAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJeChFBZGRDcmVkaXRzUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSFAoMYW1vdW50X2NlbnRzGAIgASgDEhEKBG5vdGUYAyABKAlIAIgBAUIHCgVfbm90ZSKPAQoSQWRkQ3JlZGl0c1Jlc3BvbnNlEkIKDG9yZ2FuaXphdGlvbhgBIAEoCzIsLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Pcmdhbml6YXRpb24SGQoRbmV3X2JhbGFuY2VfY2VudHMYAiABKAMSGgoSYW1vdW50X2FkZGVkX2NlbnRzGAMgASgDImMKFkFkbWluQWRkQ3JlZGl0c1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhQKDGFtb3VudF9jZW50cxgCIAEoAxIRCgRub3RlGAMgASgJSACIAQFCBwoFX25vdGUilAEKF0FkbWluQWRkQ3JlZGl0c1Jlc3BvbnNlEkIKDG9yZ2FuaXphdGlvbhgBIAEoCzIsLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Pcmdhbml6YXRpb24SGQoRbmV3X2JhbGFuY2VfY2VudHMYAiABKAMSGgoSYW1vdW50X2FkZGVkX2NlbnRzGAMgASgDImYKGUFkbWluUmVtb3ZlQ3JlZGl0c1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhQKDGFtb3VudF9jZW50cxgCIAEoAxIRCgRub3RlGAMgASgJSACIAQFCBwoFX25vdGUimQEKGkFkbWluUmVtb3ZlQ3JlZGl0c1Jlc3BvbnNlEkIKDG9yZ2FuaXphdGlvbhgBIAEoCzIsLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Pcmdhbml6YXRpb24SGQoRbmV3X2JhbGFuY2VfY2VudHMYAiABKAMSHAoUYW1vdW50X3JlbW92ZWRfY2VudHMYAyABKAMiTgoTR2V0Q3JlZGl0TG9nUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSDAoEcGFnZRgCIAEoBRIQCghwZXJfcGFnZRgDIAEoBSKYAQoUR2V0Q3JlZGl0TG9nUmVzcG9uc2USRwoMdHJhbnNhY3Rpb25zGAEgAygLMjEub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkNyZWRpdFRyYW5zYWN0aW9uEjcKCnBhZ2luYXRpb24YAiABKAsyIy5vYmllbnRlLmNsb3VkLmNvbW1vbi52MS5QYWdpbmF0aW9uIvcBChFDcmVkaXRUcmFuc2FjdGlvbhIKCgJpZBgBIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAiABKAkSFAoMYW1vdW50X2NlbnRzGAMgASgDEhUKDWJhbGFuY2VfYWZ0ZXIYBCABKAMSDAoEdHlwZRgFIAEoCRIOCgZzb3VyY2UYBiABKAkSEQoEbm90ZRgHIAEoCUgAiAEBEhcKCmNyZWF0ZWRfYnkYCCABKAlIAYgBARIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEIHCgVfbm90ZUINCgtfY3JlYXRlZF9ieSIyChdHZXRNeVBlcm1pc3Npb25zUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkiLwoYR2V0TXlQZXJtaXNzaW9uc1Jlc3BvbnNlEhMKC3Blcm1pc3Npb25zGAEgAygJMqETChNPcmdhbml6YXRpb25TZXJ2aWNlEogBChFMaXN0T3JnYW5pemF0aW9ucxI4Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5MaXN0T3JnYW5pemF0aW9uc1JlcXVlc3QaOS5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuTGlzdE9yZ2FuaXphdGlvbnNSZXNwb25zZRKLAQoSQ3JlYXRlT3JnYW5pemF0aW9uEjkub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkNyZWF0ZU9yZ2FuaXphdGlvblJlcXVlc3QaOi5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuQ3JlYXRlT3JnYW5pemF0aW9uUmVzcG9uc2USggEKD0dldE9yZ2FuaXphdGlvbhI2Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5HZXRPcmdhbml6YXRpb25SZXF1ZXN0Gjcub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkdldE9yZ2FuaXphdGlvblJlc3BvbnNlEosBChJVcGRhdGVPcmdhbml6YXRpb24SOS5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuVXBkYXRlT3JnYW5pemF0aW9uUmVxdWVzdBo6Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5VcGRhdGVPcmdhbml6YXRpb25SZXNwb25zZRJ2CgtMaXN0TWVtYmVycxIyLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5MaXN0TWVtYmVyc1JlcXVlc3QaMy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuTGlzdE1lbWJlcnNSZXNwb25zZRJ5CgxJbnZpdGVNZW1iZXISMy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuSW52aXRlTWVtYmVyUmVxdWVzdBo0Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5JbnZpdGVNZW1iZXJSZXNwb25zZRJ5CgxSZXNlbmRJbnZpdGUSMy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuUmVzZW5kSW52aXRlUmVxdWVzdBo0Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5SZXNlbmRJbnZpdGVSZXNwb25zZRJ8Cg1MaXN0TXlJbnZpdGVzEjQub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkxpc3RNeUludml0ZXNSZXF1ZXN0GjUub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkxpc3RNeUludml0ZXNSZXNwb25zZRJ5CgxBY2NlcHRJbnZpdGUSMy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuQWNjZXB0SW52aXRlUmVxdWVzdBo0Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5BY2NlcHRJbnZpdGVSZXNwb25zZRJ8Cg1EZWNsaW5lSW52aXRlEjQub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkRlY2xpbmVJbnZpdGVSZXF1ZXN0GjUub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkRlY2xpbmVJbnZpdGVSZXNwb25zZRJ5CgxVcGRhdGVNZW1iZXISMy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuVXBkYXRlTWVtYmVyUmVxdWVzdBo0Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5VcGRhdGVNZW1iZXJSZXNwb25zZRJ5CgxSZW1vdmVNZW1iZXISMy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuUmVtb3ZlTWVtYmVyUmVxdWVzdBo0Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5SZW1vdmVNZW1iZXJSZXNwb25zZRKIAQoRVHJhbnNmZXJPd25lcnNoaXASOC5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuVHJhbnNmZXJPd25lcnNoaXBSZXF1ZXN0Gjkub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLlRyYW5zZmVyT3duZXJzaGlwUmVzcG9uc2USbQoIR2V0VXNhZ2USLy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuR2V0VXNhZ2VSZXF1ZXN0GjAub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkdldFVzYWdlUmVzcG9uc2UScwoKQWRkQ3JlZGl0cxIxLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5BZGRDcmVkaXRzUmVxdWVzdBoyLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5BZGRDcmVkaXRzUmVzcG9uc2USggEKD0FkbWluQWRkQ3JlZGl0cxI2Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5BZG1pbkFkZENyZWRpdHNSZXF1ZXN0Gjcub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkFkbWluQWRkQ3JlZGl0c1Jlc3BvbnNlEosBChJBZG1pblJlbW92ZUNyZWRpdHMSOS5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuQWRtaW5SZW1vdmVDcmVkaXRzUmVxdWVzdBo6Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5BZG1pblJlbW92ZUNyZWRpdHNSZXNwb25zZRJ5CgxHZXRDcmVkaXRMb2cSMy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuR2V0Q3JlZGl0TG9nUmVxdWVzdBo0Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5HZXRDcmVkaXRMb2dSZXNwb25zZRKFAQoQR2V0TXlQZXJtaXNzaW9ucxI3Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5HZXRNeVBlcm1pc3Npb25zUmVxdWVzdBo4Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5HZXRNeVBlcm1pc3Npb25zUmVzcG9uc2VCW1pZZ2l0aHViLmNvbS9vYmllbnRlL2Nsb3VkL2FwcHMvc2hhcmVkL3Byb3RvL29iaWVudGUvY2xvdWQvb3JnYW5pemF0aW9ucy92MTtvcmdhbml6YXRpb25zdjFiBnByb3RvMw", [file_google_protobuf_timestamp, file_obiente_cloud_auth_v1_auth_service, file_obiente_cloud_common_v1_common]); + fileDesc("CjlvYmllbnRlL2Nsb3VkL29yZ2FuaXphdGlvbnMvdjEvb3JnYW5pemF0aW9uX3NlcnZpY2UucHJvdG8SHm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MSJICg9HZXRVc2FnZVJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhIKBW1vbnRoGAIgASgJSACIAQFCCAoGX21vbnRoIv0BChBHZXRVc2FnZVJlc3BvbnNlEhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRINCgVtb250aBgCIAEoCRI9CgdjdXJyZW50GAMgASgLMiwub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLlVzYWdlTWV0cmljcxJHChFlc3RpbWF0ZWRfbW9udGhseRgEIAEoCzIsLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Vc2FnZU1ldHJpY3MSOQoFcXVvdGEYBSABKAsyKi5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuVXNhZ2VRdW90YSLpAwoMVXNhZ2VNZXRyaWNzEhgKEGNwdV9jb3JlX3NlY29uZHMYASABKAMSGwoTbWVtb3J5X2J5dGVfc2Vjb25kcxgCIAEoAxIaChJiYW5kd2lkdGhfcnhfYnl0ZXMYAyABKAMSGgoSYmFuZHdpZHRoX3R4X2J5dGVzGAQgASgDEhUKDXN0b3JhZ2VfYnl0ZXMYBSABKAMSHwoXZGVwbG95bWVudHNfYWN0aXZlX3BlYWsYBiABKAUSHAoUZXN0aW1hdGVkX2Nvc3RfY2VudHMYByABKAMSGwoOY3B1X2Nvc3RfY2VudHMYCCABKANIAIgBARIeChFtZW1vcnlfY29zdF9jZW50cxgJIAEoA0gBiAEBEiEKFGJhbmR3aWR0aF9jb3N0X2NlbnRzGAogASgDSAKIAQESHwoSc3RvcmFnZV9jb3N0X2NlbnRzGAsgASgDSAOIAQESIQoUcHVibGljX2lwX2Nvc3RfY2VudHMYDCABKANIBIgBAUIRCg9fY3B1X2Nvc3RfY2VudHNCFAoSX21lbW9yeV9jb3N0X2NlbnRzQhcKFV9iYW5kd2lkdGhfY29zdF9jZW50c0IVChNfc3RvcmFnZV9jb3N0X2NlbnRzQhcKFV9wdWJsaWNfaXBfY29zdF9jZW50cyKkAQoKVXNhZ2VRdW90YRIgChhjcHVfY29yZV9zZWNvbmRzX21vbnRobHkYASABKAMSIwobbWVtb3J5X2J5dGVfc2Vjb25kc19tb250aGx5GAIgASgDEh8KF2JhbmR3aWR0aF9ieXRlc19tb250aGx5GAMgASgDEhUKDXN0b3JhZ2VfYnl0ZXMYBCABKAMSFwoPZGVwbG95bWVudHNfbWF4GAUgASgFImAKGExpc3RPcmdhbml6YXRpb25zUmVxdWVzdBIMCgRwYWdlGAEgASgFEhAKCHBlcl9wYWdlGAIgASgFEhYKCW9ubHlfbWluZRgDIAEoCEgAiAEBQgwKCl9vbmx5X21pbmUimQEKGUxpc3RPcmdhbml6YXRpb25zUmVzcG9uc2USQwoNb3JnYW5pemF0aW9ucxgBIAMoCzIsLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Pcmdhbml6YXRpb24SNwoKcGFnaW5hdGlvbhgCIAEoCzIjLm9iaWVudGUuY2xvdWQuY29tbW9uLnYxLlBhZ2luYXRpb24iRQoZQ3JlYXRlT3JnYW5pemF0aW9uUmVxdWVzdBIMCgRuYW1lGAEgASgJEgwKBHNsdWcYAiABKAkSDAoEcGxhbhgDIAEoCSJgChpDcmVhdGVPcmdhbml6YXRpb25SZXNwb25zZRJCCgxvcmdhbml6YXRpb24YASABKAsyLC5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuT3JnYW5pemF0aW9uIjEKFkdldE9yZ2FuaXphdGlvblJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJIl0KF0dldE9yZ2FuaXphdGlvblJlc3BvbnNlEkIKDG9yZ2FuaXphdGlvbhgBIAEoCzIsLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Pcmdhbml6YXRpb24icAoZVXBkYXRlT3JnYW5pemF0aW9uUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSEQoEbmFtZRgCIAEoCUgAiAEBEhMKBmRvbWFpbhgDIAEoCUgBiAEBQgcKBV9uYW1lQgkKB19kb21haW4iYAoaVXBkYXRlT3JnYW5pemF0aW9uUmVzcG9uc2USQgoMb3JnYW5pemF0aW9uGAEgASgLMiwub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLk9yZ2FuaXphdGlvbiJNChJMaXN0TWVtYmVyc1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEgwKBHBhZ2UYAiABKAUSEAoIcGVyX3BhZ2UYAyABKAUikwEKE0xpc3RNZW1iZXJzUmVzcG9uc2USQwoHbWVtYmVycxgBIAMoCzIyLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Pcmdhbml6YXRpb25NZW1iZXISNwoKcGFnaW5hdGlvbhgCIAEoCzIjLm9iaWVudGUuY2xvdWQuY29tbW9uLnYxLlBhZ2luYXRpb24iSwoTSW52aXRlTWVtYmVyUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSDQoFZW1haWwYAiABKAkSDAoEcm9sZRgDIAEoCSJaChRJbnZpdGVNZW1iZXJSZXNwb25zZRJCCgZtZW1iZXIYASABKAsyMi5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuT3JnYW5pemF0aW9uTWVtYmVyIkEKE1Jlc2VuZEludml0ZVJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhEKCW1lbWJlcl9pZBgCIAEoCSInChRSZXNlbmRJbnZpdGVSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIjYKFExpc3RNeUludml0ZXNSZXF1ZXN0EgwKBHBhZ2UYASABKAUSEAoIcGVyX3BhZ2UYAiABKAUikAEKFUxpc3RNeUludml0ZXNSZXNwb25zZRI+CgdpbnZpdGVzGAEgAygLMi0ub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLlBlbmRpbmdJbnZpdGUSNwoKcGFnaW5hdGlvbhgCIAEoCzIjLm9iaWVudGUuY2xvdWQuY29tbW9uLnYxLlBhZ2luYXRpb24ipAEKDVBlbmRpbmdJbnZpdGUSCgoCaWQYASABKAkSFwoPb3JnYW5pemF0aW9uX2lkGAIgASgJEhkKEW9yZ2FuaXphdGlvbl9uYW1lGAMgASgJEgwKBHJvbGUYBCABKAkSLgoKaW52aXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFQoNaW52aXRlcl9lbWFpbBgGIAEoCSJBChNBY2NlcHRJbnZpdGVSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIRCgltZW1iZXJfaWQYAiABKAkingEKFEFjY2VwdEludml0ZVJlc3BvbnNlEkIKBm1lbWJlchgBIAEoCzIyLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Pcmdhbml6YXRpb25NZW1iZXISQgoMb3JnYW5pemF0aW9uGAIgASgLMiwub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLk9yZ2FuaXphdGlvbiJCChREZWNsaW5lSW52aXRlUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSEQoJbWVtYmVyX2lkGAIgASgJIigKFURlY2xpbmVJbnZpdGVSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIl0KE1VwZGF0ZU1lbWJlclJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhEKCW1lbWJlcl9pZBgCIAEoCRIRCgRyb2xlGAMgASgJSACIAQFCBwoFX3JvbGUiWgoUVXBkYXRlTWVtYmVyUmVzcG9uc2USQgoGbWVtYmVyGAEgASgLMjIub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLk9yZ2FuaXphdGlvbk1lbWJlciJBChNSZW1vdmVNZW1iZXJSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIRCgltZW1iZXJfaWQYAiABKAkiJwoUUmVtb3ZlTWVtYmVyUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCJnChhUcmFuc2Zlck93bmVyc2hpcFJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhsKE25ld19vd25lcl9tZW1iZXJfaWQYAiABKAkSFQoNZmFsbGJhY2tfcm9sZRgDIAEoCSKCAQoZVHJhbnNmZXJPd25lcnNoaXBSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEiAKGHByZXZpb3VzX293bmVyX21lbWJlcl9pZBgCIAEoCRIbChNuZXdfb3duZXJfbWVtYmVyX2lkGAMgASgJEhUKDWZhbGxiYWNrX3JvbGUYBCABKAki7QIKDE9yZ2FuaXphdGlvbhIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEgwKBHNsdWcYAyABKAkSEwoGZG9tYWluGAQgASgJSACIAQESDAoEcGxhbhgFIAEoCRIOCgZzdGF0dXMYBiABKAkSLgoKY3JlYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFwoPbWF4X2RlcGxveW1lbnRzGAggASgFEhkKEW1heF92cHNfaW5zdGFuY2VzGAkgASgFEhgKEG1heF90ZWFtX21lbWJlcnMYCiABKAUSDwoHY3JlZGl0cxgLIAEoAxJACglwbGFuX2luZm8YDCABKAsyKC5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuUGxhbkluZm9IAYgBARIYChB0b3RhbF9wYWlkX2NlbnRzGA0gASgDQgkKB19kb21haW5CDAoKX3BsYW5faW5mbyKtAgoIUGxhbkluZm8SDwoHcGxhbl9pZBgBIAEoCRIRCglwbGFuX25hbWUYAiABKAkSEwoLZGVzY3JpcHRpb24YAyABKAkSEQoJY3B1X2NvcmVzGAQgASgFEhQKDG1lbW9yeV9ieXRlcxgFIAEoAxIXCg9kZXBsb3ltZW50c19tYXgYBiABKAUSGQoRbWF4X3Zwc19pbnN0YW5jZXMYCyABKAUSHQoVYmFuZHdpZHRoX2J5dGVzX21vbnRoGAcgASgDEhUKDXN0b3JhZ2VfYnl0ZXMYCCABKAMSHQoVbWluaW11bV9wYXltZW50X2NlbnRzGAkgASgDEiIKGm1vbnRobHlfZnJlZV9jcmVkaXRzX2NlbnRzGAogASgDEhIKCnRyaWFsX2RheXMYDCABKAUimAEKEk9yZ2FuaXphdGlvbk1lbWJlchIKCgJpZBgBIAEoCRIpCgR1c2VyGAIgASgLMhsub2JpZW50ZS5jbG91ZC5hdXRoLnYxLlVzZXISDAoEcm9sZRgDIAEoCRIOCgZzdGF0dXMYBCABKAkSLQoJam9pbmVkX2F0GAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJeChFBZGRDcmVkaXRzUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSFAoMYW1vdW50X2NlbnRzGAIgASgDEhEKBG5vdGUYAyABKAlIAIgBAUIHCgVfbm90ZSKPAQoSQWRkQ3JlZGl0c1Jlc3BvbnNlEkIKDG9yZ2FuaXphdGlvbhgBIAEoCzIsLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Pcmdhbml6YXRpb24SGQoRbmV3X2JhbGFuY2VfY2VudHMYAiABKAMSGgoSYW1vdW50X2FkZGVkX2NlbnRzGAMgASgDImMKFkFkbWluQWRkQ3JlZGl0c1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhQKDGFtb3VudF9jZW50cxgCIAEoAxIRCgRub3RlGAMgASgJSACIAQFCBwoFX25vdGUilAEKF0FkbWluQWRkQ3JlZGl0c1Jlc3BvbnNlEkIKDG9yZ2FuaXphdGlvbhgBIAEoCzIsLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Pcmdhbml6YXRpb24SGQoRbmV3X2JhbGFuY2VfY2VudHMYAiABKAMSGgoSYW1vdW50X2FkZGVkX2NlbnRzGAMgASgDImYKGUFkbWluUmVtb3ZlQ3JlZGl0c1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhQKDGFtb3VudF9jZW50cxgCIAEoAxIRCgRub3RlGAMgASgJSACIAQFCBwoFX25vdGUimQEKGkFkbWluUmVtb3ZlQ3JlZGl0c1Jlc3BvbnNlEkIKDG9yZ2FuaXphdGlvbhgBIAEoCzIsLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5Pcmdhbml6YXRpb24SGQoRbmV3X2JhbGFuY2VfY2VudHMYAiABKAMSHAoUYW1vdW50X3JlbW92ZWRfY2VudHMYAyABKAMiTgoTR2V0Q3JlZGl0TG9nUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSDAoEcGFnZRgCIAEoBRIQCghwZXJfcGFnZRgDIAEoBSKYAQoUR2V0Q3JlZGl0TG9nUmVzcG9uc2USRwoMdHJhbnNhY3Rpb25zGAEgAygLMjEub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkNyZWRpdFRyYW5zYWN0aW9uEjcKCnBhZ2luYXRpb24YAiABKAsyIy5vYmllbnRlLmNsb3VkLmNvbW1vbi52MS5QYWdpbmF0aW9uIvcBChFDcmVkaXRUcmFuc2FjdGlvbhIKCgJpZBgBIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAiABKAkSFAoMYW1vdW50X2NlbnRzGAMgASgDEhUKDWJhbGFuY2VfYWZ0ZXIYBCABKAMSDAoEdHlwZRgFIAEoCRIOCgZzb3VyY2UYBiABKAkSEQoEbm90ZRgHIAEoCUgAiAEBEhcKCmNyZWF0ZWRfYnkYCCABKAlIAYgBARIuCgpjcmVhdGVkX2F0GAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEIHCgVfbm90ZUINCgtfY3JlYXRlZF9ieSIyChdHZXRNeVBlcm1pc3Npb25zUmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkiLwoYR2V0TXlQZXJtaXNzaW9uc1Jlc3BvbnNlEhMKC3Blcm1pc3Npb25zGAEgAygJIj8KE0FkbWluU2V0UGxhblJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEg8KB3BsYW5faWQYAiABKAkiawoUQWRtaW5TZXRQbGFuUmVzcG9uc2USQgoMb3JnYW5pemF0aW9uGAEgASgLMiwub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLk9yZ2FuaXphdGlvbhIPCgdwbGFuX2lkGAIgASgJMpwUChNPcmdhbml6YXRpb25TZXJ2aWNlEnkKDEFkbWluU2V0UGxhbhIzLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5BZG1pblNldFBsYW5SZXF1ZXN0GjQub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkFkbWluU2V0UGxhblJlc3BvbnNlEogBChFMaXN0T3JnYW5pemF0aW9ucxI4Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5MaXN0T3JnYW5pemF0aW9uc1JlcXVlc3QaOS5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuTGlzdE9yZ2FuaXphdGlvbnNSZXNwb25zZRKLAQoSQ3JlYXRlT3JnYW5pemF0aW9uEjkub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkNyZWF0ZU9yZ2FuaXphdGlvblJlcXVlc3QaOi5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuQ3JlYXRlT3JnYW5pemF0aW9uUmVzcG9uc2USggEKD0dldE9yZ2FuaXphdGlvbhI2Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5HZXRPcmdhbml6YXRpb25SZXF1ZXN0Gjcub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkdldE9yZ2FuaXphdGlvblJlc3BvbnNlEosBChJVcGRhdGVPcmdhbml6YXRpb24SOS5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuVXBkYXRlT3JnYW5pemF0aW9uUmVxdWVzdBo6Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5VcGRhdGVPcmdhbml6YXRpb25SZXNwb25zZRJ2CgtMaXN0TWVtYmVycxIyLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5MaXN0TWVtYmVyc1JlcXVlc3QaMy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuTGlzdE1lbWJlcnNSZXNwb25zZRJ5CgxJbnZpdGVNZW1iZXISMy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuSW52aXRlTWVtYmVyUmVxdWVzdBo0Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5JbnZpdGVNZW1iZXJSZXNwb25zZRJ5CgxSZXNlbmRJbnZpdGUSMy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuUmVzZW5kSW52aXRlUmVxdWVzdBo0Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5SZXNlbmRJbnZpdGVSZXNwb25zZRJ8Cg1MaXN0TXlJbnZpdGVzEjQub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkxpc3RNeUludml0ZXNSZXF1ZXN0GjUub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkxpc3RNeUludml0ZXNSZXNwb25zZRJ5CgxBY2NlcHRJbnZpdGUSMy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuQWNjZXB0SW52aXRlUmVxdWVzdBo0Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5BY2NlcHRJbnZpdGVSZXNwb25zZRJ8Cg1EZWNsaW5lSW52aXRlEjQub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkRlY2xpbmVJbnZpdGVSZXF1ZXN0GjUub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkRlY2xpbmVJbnZpdGVSZXNwb25zZRJ5CgxVcGRhdGVNZW1iZXISMy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuVXBkYXRlTWVtYmVyUmVxdWVzdBo0Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5VcGRhdGVNZW1iZXJSZXNwb25zZRJ5CgxSZW1vdmVNZW1iZXISMy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuUmVtb3ZlTWVtYmVyUmVxdWVzdBo0Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5SZW1vdmVNZW1iZXJSZXNwb25zZRKIAQoRVHJhbnNmZXJPd25lcnNoaXASOC5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuVHJhbnNmZXJPd25lcnNoaXBSZXF1ZXN0Gjkub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLlRyYW5zZmVyT3duZXJzaGlwUmVzcG9uc2USbQoIR2V0VXNhZ2USLy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuR2V0VXNhZ2VSZXF1ZXN0GjAub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkdldFVzYWdlUmVzcG9uc2UScwoKQWRkQ3JlZGl0cxIxLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5BZGRDcmVkaXRzUmVxdWVzdBoyLm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5BZGRDcmVkaXRzUmVzcG9uc2USggEKD0FkbWluQWRkQ3JlZGl0cxI2Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5BZG1pbkFkZENyZWRpdHNSZXF1ZXN0Gjcub2JpZW50ZS5jbG91ZC5vcmdhbml6YXRpb25zLnYxLkFkbWluQWRkQ3JlZGl0c1Jlc3BvbnNlEosBChJBZG1pblJlbW92ZUNyZWRpdHMSOS5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuQWRtaW5SZW1vdmVDcmVkaXRzUmVxdWVzdBo6Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5BZG1pblJlbW92ZUNyZWRpdHNSZXNwb25zZRJ5CgxHZXRDcmVkaXRMb2cSMy5vYmllbnRlLmNsb3VkLm9yZ2FuaXphdGlvbnMudjEuR2V0Q3JlZGl0TG9nUmVxdWVzdBo0Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5HZXRDcmVkaXRMb2dSZXNwb25zZRKFAQoQR2V0TXlQZXJtaXNzaW9ucxI3Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5HZXRNeVBlcm1pc3Npb25zUmVxdWVzdBo4Lm9iaWVudGUuY2xvdWQub3JnYW5pemF0aW9ucy52MS5HZXRNeVBlcm1pc3Npb25zUmVzcG9uc2VCW1pZZ2l0aHViLmNvbS9vYmllbnRlL2Nsb3VkL2FwcHMvc2hhcmVkL3Byb3RvL29iaWVudGUvY2xvdWQvb3JnYW5pemF0aW9ucy92MTtvcmdhbml6YXRpb25zdjFiBnByb3RvMw", [file_google_protobuf_timestamp, file_obiente_cloud_auth_v1_auth_service, file_obiente_cloud_common_v1_common]); /** * @generated from message obiente.cloud.organizations.v1.GetUsageRequest @@ -1367,10 +1367,70 @@ export type GetMyPermissionsResponse = Message<"obiente.cloud.organizations.v1.G export const GetMyPermissionsResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_obiente_cloud_organizations_v1_organization_service, 44); +/** + * Request to set the active plan for an organization (superadmin only) + * + * @generated from message obiente.cloud.organizations.v1.AdminSetPlanRequest + */ +export type AdminSetPlanRequest = Message<"obiente.cloud.organizations.v1.AdminSetPlanRequest"> & { + /** + * @generated from field: string organization_id = 1; + */ + organizationId: string; + + /** + * The plan to assign (must match OrganizationPlan.ID) + * + * @generated from field: string plan_id = 2; + */ + planId: string; +}; + +/** + * Describes the message obiente.cloud.organizations.v1.AdminSetPlanRequest. + * Use `create(AdminSetPlanRequestSchema)` to create a new message. + */ +export const AdminSetPlanRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_obiente_cloud_organizations_v1_organization_service, 45); + +/** + * Response for AdminSetPlan + * + * @generated from message obiente.cloud.organizations.v1.AdminSetPlanResponse + */ +export type AdminSetPlanResponse = Message<"obiente.cloud.organizations.v1.AdminSetPlanResponse"> & { + /** + * @generated from field: obiente.cloud.organizations.v1.Organization organization = 1; + */ + organization?: Organization; + + /** + * @generated from field: string plan_id = 2; + */ + planId: string; +}; + +/** + * Describes the message obiente.cloud.organizations.v1.AdminSetPlanResponse. + * Use `create(AdminSetPlanResponseSchema)` to create a new message. + */ +export const AdminSetPlanResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_obiente_cloud_organizations_v1_organization_service, 46); + /** * @generated from service obiente.cloud.organizations.v1.OrganizationService */ export const OrganizationService: GenService<{ + /** + * Admin: Set the active plan for an organization (superadmin only) + * + * @generated from rpc obiente.cloud.organizations.v1.OrganizationService.AdminSetPlan + */ + adminSetPlan: { + methodKind: "unary"; + input: typeof AdminSetPlanRequestSchema; + output: typeof AdminSetPlanResponseSchema; + }, /** * List user's organizations * From e084e327776ed906a9380e5cca0fe26833bd9c53 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Sun, 1 Feb 2026 04:54:29 +0100 Subject: [PATCH 2/3] feat(orgs): add AdminSetPlan RPC to set org plan Add AdminSetPlan handler to organizations service to allow superadmins to change an organization's active plan. - Authenticate user and require superadmin privilege. - Validate organization_id and plan_id request fields. - Update organizations.plan via a direct DB Exec. - Re-fetch updated organization row (matching ListOrganizations' row shape) and build database.Organization to convert to proto. - Return updated Organization proto and the applied plan_id. - Surface clear connect errors for unauthenticated, forbidden, invalid arguments and backend failures. This provides an administrative endpoint for plan changes and reuses existing row mapping to ensure consistent response shape. --- .../internal/service/service.go | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/apps/organizations-service/internal/service/service.go b/apps/organizations-service/internal/service/service.go index 23b81c19..22c65fef 100644 --- a/apps/organizations-service/internal/service/service.go +++ b/apps/organizations-service/internal/service/service.go @@ -2111,3 +2111,69 @@ func (s *Service) GetMyPermissions(ctx context.Context, req *connect.Request[org Permissions: permissions, }), nil } + +// AdminSetPlan sets the active plan for an organization (superadmin only) +func (s *Service) AdminSetPlan(ctx context.Context, req *connect.Request[organizationsv1.AdminSetPlanRequest]) (*connect.Response[organizationsv1.AdminSetPlanResponse], error) { + // Authenticate the user + user, err := auth.GetUserFromContext(ctx) + if err != nil { + return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("unauthenticated")) + } + + // Ensure the user is a superadmin + if !auth.IsSuperadmin(ctx, user) { + return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied")) + } + + // Validate the organization and plan IDs + orgID := req.Msg.GetOrganizationId() + planID := req.Msg.GetPlanId() + if orgID == "" || planID == "" { + return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("organization_id and plan_id are required")) + } + + // Update the organization's plan in the database + result := database.DB.Exec(` + UPDATE organizations + SET plan = ? + WHERE id = ? + `, planID, orgID) + if result.Error != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to update organization plan: %w", result.Error)) + } + + // Fetch the updated organization using the same row pattern as ListOrganizations + type row struct { + Id, Name, Slug, Plan, Status string + Domain *string + Credits int64 + TotalPaidCents int64 + CreatedAt time.Time + } + var r row + err = database.DB.Raw(` + SELECT id, name, slug, plan, status, domain, credits, total_paid_cents, created_at + FROM organizations + WHERE id = ? + `, orgID).Scan(&r).Error + if err != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to fetch updated organization: %w", err)) + } + + org := &database.Organization{ + ID: r.Id, + Name: r.Name, + Slug: r.Slug, + Plan: r.Plan, + Status: r.Status, + Domain: r.Domain, + Credits: r.Credits, + TotalPaidCents: r.TotalPaidCents, + CreatedAt: r.CreatedAt, + } + + return connect.NewResponse(&organizationsv1.AdminSetPlanResponse{ + Organization: organizationToProto(org), + PlanId: planID, + }), nil +} From 3ec4bae57478e6e171aca2306483673a01a436d9 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Sun, 1 Feb 2026 04:55:27 +0100 Subject: [PATCH 3/3] feat(superadmin): add "Set Plan" dialog and plan selection Add a Set Plan dialog to the superadmin organizations page, enabling admins to view and change an organization's plan from the UI. - Add OuiDialog UI for "Set Plan" with organization and current plan display, a plan select, resources preview, and footer actions (Cancel / Set Plan). - Wire dialog state and selection: setPlanDialogOpen, selectedPlanId, selectedPlanInfo, setPlanLoading, selectedOrgId, selectedOrgName, selectedOrgCurrentPlan, and planSelectItems. - Import SuperadminService client and initialize saClient via useConnectClient for future set-plan operations. - Add manageOrgId ref and cleanup an unused manageCreditsOrgId ref (rename/cleanup related to credits management). - Display formatted resource info (CPU, memory, deployments, VPS, monthly credits) and use existing helpers (prettyPlan, formatBytes, OuiCurrency). These changes provide an interactive way to inspect plan resource details and assign plans to organizations, preparing the codepath for server-side plan update integration. --- CLAUDE.md | 95 ++++++++++++ .../pages/superadmin/organizations/index.vue | 138 +++++++++++++++++- apps/dashboard/nuxt.config.ts | 5 +- 3 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..1512871d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,95 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Obiente Cloud is a distributed Platform-as-a-Service (PaaS) built as an Nx monorepo with 14 Go microservices and a Nuxt 4 dashboard. Services communicate via ConnectRPC (protocol buffers). Deployed on Docker Swarm. + +## Build & Development Commands + +### Package Management +```bash +pnpm install # Install all JS/TS dependencies +``` + +### Dashboard (Nuxt 4 frontend) +```bash +nx serve dashboard # Dev server on port 3000 +nx nuxt:build dashboard # Production build +nx lint dashboard # ESLint +nx typecheck dashboard # Type checking +``` + +### Go Services +```bash +cd apps/ +go run main.go # Run locally +go build # Build binary +go test ./... # Run tests +``` + +### Protocol Buffers +```bash +cd packages/proto +pnpm build # Regenerate all proto code (buf generate) +``` +Generated Go code goes to `apps/shared/proto/`, TypeScript to `packages/proto/src/generated/`. + +### Docker +```bash +docker compose up -d # Local dev (all services) +docker build -f apps//Dockerfile -t ghcr.io/obiente/cloud-:latest . # Build image +./scripts/deploy-swarm-dev.sh # Swarm dev deploy +./scripts/deploy-swarm-dev.sh -b # Build + deploy +``` + +### Nx +Always prefer running tasks through `nx` rather than underlying tooling directly. Use `nx run`, `nx run-many`, `nx affected`. + +## Architecture + +### Service Ports +| Service | Port | +|---------|------| +| Dashboard | 3000 | +| API Gateway | 3001 | +| Auth | 3002 | +| Organizations | 3003 | +| Billing | 3004 | +| Deployments | 3005 | +| GameServers | 3006 | +| Orchestrator | 3007 | +| VPS | 3008 | +| Support | 3009 | +| Audit | 3010 | +| Superadmin | 3011 | +| Notifications | 3012 | +| DNS | 8053 | + +### Key Architectural Patterns + +- **API Gateway** routes all external requests to backend services. Supports both direct service routing and Traefik-based routing. +- **ConnectRPC** is used for all inter-service communication. Proto definitions live in `packages/proto/proto/obiente/cloud/`. Buf generates both Go and TypeScript clients. +- **Go workspace** (`go.work`) links all 15 Go modules. Shared code is in `apps/shared/` with packages for auth, database, docker, middleware, orchestrator, quota, etc. +- **Auth** is handled via Zitadel integration with RBAC. The auth-service validates tokens and manages roles/permissions. +- **Orchestrator** handles intelligent node selection and load balancing across the Docker Swarm cluster. +- **Database**: PostgreSQL (primary), TimescaleDB (metrics/audit), Redis (cache, build logs). +- **Dashboard** uses Nuxt 4, Vue 3, Tailwind CSS v4, Pinia for state, Ark UI for components, and `@connectrpc/connect-web` for API calls. + +### Monorepo Structure +- `apps/` - All microservices + dashboard +- `packages/proto/` - Protobuf definitions and generated code +- `packages/database/` - Drizzle ORM schemas and migrations +- `packages/config/` - Shared ESLint, Prettier, TypeScript configs +- `packages/types/` - Shared TypeScript types +- `tools/nxsh/` - Custom Nx shell executor +- `monitoring/` - Prometheus & Grafana configs +- `scripts/` - Deployment and operational scripts + +### Docker Compose Files +- `docker-compose.yml` - Local development +- `docker-compose.base.yml` - Shared env vars (YAML anchors) +- `docker-compose.swarm.yml` - Production swarm +- `docker-compose.swarm.dev.yml` - Dev swarm (must use `docker stack deploy`, not `docker compose`) +- `docker-compose.swarm.ha.yml` - HA production with PostgreSQL cluster diff --git a/apps/dashboard/app/pages/superadmin/organizations/index.vue b/apps/dashboard/app/pages/superadmin/organizations/index.vue index 110b3527..4803fb5c 100644 --- a/apps/dashboard/app/pages/superadmin/organizations/index.vue +++ b/apps/dashboard/app/pages/superadmin/organizations/index.vue @@ -130,6 +130,61 @@ + + + + + + Organization + {{ selectedOrgName }} + + + + Current Plan + + {{ prettyPlan(selectedOrgCurrentPlan) || 'None' }} + + + + + + + + + + diff --git a/apps/dashboard/nuxt.config.ts b/apps/dashboard/nuxt.config.ts index 4d28e7c1..115427e2 100644 --- a/apps/dashboard/nuxt.config.ts +++ b/apps/dashboard/nuxt.config.ts @@ -168,7 +168,7 @@ export default defineNuxtConfig({ // Use API Gateway for all requests (routes to microservices) // When running locally (not in Docker), use localhost with Traefik port // When running in Docker, use api-gateway service name - apiHostInternal: process.env.NUXT_API_HOST_INTERNAL || process.env.NUXT_PUBLIC_API_HOST || "http://localhost:80", + apiHostInternal: process.env.NUXT_API_HOST_INTERNAL || process.env.NUXT_PUBLIC_API_HOST || "http://api.localhost", githubClientSecret: process.env.GITHUB_CLIENT_SECRET || "", // Server-side only - never expose to client session: { password: "changeme_" + crypto.randomUUID(), // CHANGE THIS IN PRODUCTION, should be at least 32 characters @@ -196,6 +196,9 @@ export default defineNuxtConfig({ port: 3000, host: "0.0.0.0", }, + future: { + compatibilityVersion: 4 + }, app: { head: {