diff --git a/components/dashboard/src/data/setup.tsx b/components/dashboard/src/data/setup.tsx index 3f069e03fcb3a5..54c4fd46bcb84e 100644 --- a/components/dashboard/src/data/setup.tsx +++ b/components/dashboard/src/data/setup.tsx @@ -24,13 +24,14 @@ import * as ConfigurationClasses from "@gitpod/public-api/lib/gitpod/v1/configur import * as AuthProviderClasses from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb"; import * as EnvVarClasses from "@gitpod/public-api/lib/gitpod/v1/envvar_pb"; import * as PrebuildClasses from "@gitpod/public-api/lib/gitpod/v1/prebuild_pb"; +import * as VerificationClasses from "@gitpod/public-api/lib/gitpod/v1/verification_pb"; import * as SCMClasses from "@gitpod/public-api/lib/gitpod/v1/scm_pb"; import * as SSHClasses from "@gitpod/public-api/lib/gitpod/v1/ssh_pb"; // This is used to version the cache // If data we cache changes in a non-backwards compatible way, increment this version // That will bust any previous cache versions a client may have stored -const CACHE_VERSION = "8"; +const CACHE_VERSION = "9"; export function noPersistence(queryKey: QueryKey): QueryKey { return [...queryKey, "no-persistence"]; @@ -152,6 +153,7 @@ function initializeMessages() { ...Object.values(AuthProviderClasses), ...Object.values(EnvVarClasses), ...Object.values(PrebuildClasses), + ...Object.values(VerificationClasses), ...Object.values(SCMClasses), ...Object.values(SSHClasses), ]; diff --git a/components/dashboard/src/service/json-rpc-verification-client.ts b/components/dashboard/src/service/json-rpc-verification-client.ts new file mode 100644 index 00000000000000..dfac3543af5b5f --- /dev/null +++ b/components/dashboard/src/service/json-rpc-verification-client.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2023 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { CallOptions, PromiseClient } from "@connectrpc/connect"; +import { PartialMessage } from "@bufbuild/protobuf"; +import { VerificationService } from "@gitpod/public-api/lib/gitpod/v1/verification_connect"; +import { + SendPhoneNumberVerificationTokenRequest, + SendPhoneNumberVerificationTokenResponse, + VerifyPhoneNumberVerificationTokenRequest, + VerifyPhoneNumberVerificationTokenResponse, +} from "@gitpod/public-api/lib/gitpod/v1/verification_pb"; +import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { getGitpodService } from "./service"; +import { validate as uuidValidate } from "uuid"; + +export class JsonRpcVerificationClient implements PromiseClient { + async sendPhoneNumberVerificationToken( + request: PartialMessage, + _options?: CallOptions | undefined, + ): Promise { + if (!request.phoneNumber) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "phoneNumber is required"); + } + const info = await getGitpodService().server.sendPhoneNumberVerificationToken(request.phoneNumber); + return new SendPhoneNumberVerificationTokenResponse({ + verificationId: info.verificationId, + }); + } + + async verifyPhoneNumberVerificationToken( + request: PartialMessage, + _options?: CallOptions | undefined, + ): Promise { + if (!request.phoneNumber) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "phoneNumber is required"); + } + if (!request.verificationId || !uuidValidate(request.verificationId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "verificationId is required"); + } + if (!request.token) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "token is required"); + } + const info = await getGitpodService().server.verifyPhoneNumberVerificationToken( + request.phoneNumber, + request.token, + request.verificationId, + ); + return new VerifyPhoneNumberVerificationTokenResponse({ + verified: info, + }); + } +} diff --git a/components/dashboard/src/service/public-api.ts b/components/dashboard/src/service/public-api.ts index e9a4888219dd3b..98af53c83cc6a7 100644 --- a/components/dashboard/src/service/public-api.ts +++ b/components/dashboard/src/service/public-api.ts @@ -36,6 +36,8 @@ import { JsonRpcScmClient } from "./json-rpc-scm-client"; import { SCMService } from "@gitpod/public-api/lib/gitpod/v1/scm_connect"; import { SSHService } from "@gitpod/public-api/lib/gitpod/v1/ssh_connect"; import { JsonRpcSSHClient } from "./json-rpc-ssh-client"; +import { JsonRpcVerificationClient } from "./json-rpc-verification-client"; +import { VerificationService } from "@gitpod/public-api/lib/gitpod/v1/verification_connect"; const transport = createConnectTransport({ baseUrl: `${window.location.protocol}//${window.location.host}/public-api`, @@ -91,6 +93,11 @@ export const sshClient = createServiceClient(SSHService, { featureFlagSuffix: "ssh", }); +export const verificationClient = createServiceClient(VerificationService, { + client: new JsonRpcVerificationClient(), + featureFlagSuffix: "verification", +}); + export async function listAllProjects(opts: { orgId: string }): Promise { let pagination = { page: 1, diff --git a/components/dashboard/src/start/VerifyModal.tsx b/components/dashboard/src/start/VerifyModal.tsx index 0b9915a3aea443..77ccdfe6be69c6 100644 --- a/components/dashboard/src/start/VerifyModal.tsx +++ b/components/dashboard/src/start/VerifyModal.tsx @@ -7,13 +7,13 @@ import { useState } from "react"; import Alert, { AlertType } from "../components/Alert"; import Modal, { ModalBody, ModalFooter, ModalHeader } from "../components/Modal"; -import { getGitpodService } from "../service/service"; import PhoneInput from "react-intl-tel-input"; import "react-intl-tel-input/dist/main.css"; import "./phone-input.css"; import { Button } from "../components/Button"; import { LinkButton } from "../components/LinkButton"; import { useFeatureFlag } from "../data/featureflag-query"; +import { verificationClient } from "../service/public-api"; interface VerifyModalState { phoneNumber?: string; @@ -41,7 +41,9 @@ export function VerifyModal() { message: undefined, sending: true, }); - const resp = await getGitpodService().server.sendPhoneNumberVerificationToken(state.phoneNumber || ""); + const resp = await verificationClient.sendPhoneNumberVerificationToken({ + phoneNumber: state.phoneNumber || "", + }); setVerificationId(resp.verificationId); setState({ ...state, @@ -123,11 +125,12 @@ export function VerifyModal() { }; const verifyToken = async () => { try { - const verified = await getGitpodService().server.verifyPhoneNumberVerificationToken( - state.phoneNumber!, - state.token!, + const resp = await verificationClient.verifyPhoneNumberVerificationToken({ verificationId, - ); + token: state.token, + phoneNumber: state.phoneNumber, + }); + const verified = resp.verified; if (verified) { setState({ ...state, diff --git a/components/public-api/gitpod/v1/verification.proto b/components/public-api/gitpod/v1/verification.proto new file mode 100644 index 00000000000000..7421a04ecdd016 --- /dev/null +++ b/components/public-api/gitpod/v1/verification.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +package gitpod.v1; + +option go_package = "github.com/gitpod-io/gitpod/components/public-api/go/v1"; + +service VerificationService { + // SendPhoneNumberVerificationToken sends a verification token to the + // specified phone number. + rpc SendPhoneNumberVerificationToken(SendPhoneNumberVerificationTokenRequest) returns (SendPhoneNumberVerificationTokenResponse) {} + + // VerifyPhoneNumberVerificationToken verifies the specified verification + // token. + rpc VerifyPhoneNumberVerificationToken(VerifyPhoneNumberVerificationTokenRequest) returns (VerifyPhoneNumberVerificationTokenResponse) {} +} + +// Required fields: +// - phone_number +message SendPhoneNumberVerificationTokenRequest { + // phone_number in E.164 format + string phone_number = 1; +} + +message SendPhoneNumberVerificationTokenResponse { + // verification_id is used to VerifyPhoneNumberVerificationToken + string verification_id = 1; +} + +// Required fields: +// - phone_number +// - verification_id +// - token +message VerifyPhoneNumberVerificationTokenRequest { + // phone_number in E.164 format + string phone_number = 1; + // verification_id is returned by SendPhoneNumberVerificationToken + string verification_id = 2; + // token is the verification token from providers + string token = 3; +} + +message VerifyPhoneNumberVerificationTokenResponse { + // verified indicates if the verification was successful + bool verified = 1; +} diff --git a/components/public-api/go/v1/scm_grpc.pb.go b/components/public-api/go/v1/scm_grpc.pb.go index 540522260e418e..96b8cfdc878b90 100644 --- a/components/public-api/go/v1/scm_grpc.pb.go +++ b/components/public-api/go/v1/scm_grpc.pb.go @@ -26,9 +26,17 @@ const _ = grpc.SupportPackageIsVersion7 // // 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 SCMServiceClient interface { + // SearchSCMTokens allows clients to retrieve SCM tokens based on the + // specified host. SearchSCMTokens(ctx context.Context, in *SearchSCMTokensRequest, opts ...grpc.CallOption) (*SearchSCMTokensResponse, error) + // GuessTokenScopes allows clients to retrieve scopes their SCM token would + // require for the specified git command. GuessTokenScopes(ctx context.Context, in *GuessTokenScopesRequest, opts ...grpc.CallOption) (*GuessTokenScopesResponse, error) + // SearchRepositories allows clients to search for suggested repositories of + // SCM providers they are connected with. SearchRepositories(ctx context.Context, in *SearchRepositoriesRequest, opts ...grpc.CallOption) (*SearchRepositoriesResponse, error) + // ListSuggestedRepositories allows clients to list suggested repositories + // based on recent workspaces and accessible repo configurations. ListSuggestedRepositories(ctx context.Context, in *ListSuggestedRepositoriesRequest, opts ...grpc.CallOption) (*ListSuggestedRepositoriesResponse, error) } @@ -80,9 +88,17 @@ func (c *sCMServiceClient) ListSuggestedRepositories(ctx context.Context, in *Li // All implementations must embed UnimplementedSCMServiceServer // for forward compatibility type SCMServiceServer interface { + // SearchSCMTokens allows clients to retrieve SCM tokens based on the + // specified host. SearchSCMTokens(context.Context, *SearchSCMTokensRequest) (*SearchSCMTokensResponse, error) + // GuessTokenScopes allows clients to retrieve scopes their SCM token would + // require for the specified git command. GuessTokenScopes(context.Context, *GuessTokenScopesRequest) (*GuessTokenScopesResponse, error) + // SearchRepositories allows clients to search for suggested repositories of + // SCM providers they are connected with. SearchRepositories(context.Context, *SearchRepositoriesRequest) (*SearchRepositoriesResponse, error) + // ListSuggestedRepositories allows clients to list suggested repositories + // based on recent workspaces and accessible repo configurations. ListSuggestedRepositories(context.Context, *ListSuggestedRepositoriesRequest) (*ListSuggestedRepositoriesResponse, error) mustEmbedUnimplementedSCMServiceServer() } diff --git a/components/public-api/go/v1/v1connect/scm.connect.go b/components/public-api/go/v1/v1connect/scm.connect.go index 2cfa4e4086d811..c361543e5d0127 100644 --- a/components/public-api/go/v1/v1connect/scm.connect.go +++ b/components/public-api/go/v1/v1connect/scm.connect.go @@ -31,9 +31,17 @@ const ( // SCMServiceClient is a client for the gitpod.v1.SCMService service. type SCMServiceClient interface { + // SearchSCMTokens allows clients to retrieve SCM tokens based on the + // specified host. SearchSCMTokens(context.Context, *connect_go.Request[v1.SearchSCMTokensRequest]) (*connect_go.Response[v1.SearchSCMTokensResponse], error) + // GuessTokenScopes allows clients to retrieve scopes their SCM token would + // require for the specified git command. GuessTokenScopes(context.Context, *connect_go.Request[v1.GuessTokenScopesRequest]) (*connect_go.Response[v1.GuessTokenScopesResponse], error) + // SearchRepositories allows clients to search for suggested repositories of + // SCM providers they are connected with. SearchRepositories(context.Context, *connect_go.Request[v1.SearchRepositoriesRequest]) (*connect_go.Response[v1.SearchRepositoriesResponse], error) + // ListSuggestedRepositories allows clients to list suggested repositories + // based on recent workspaces and accessible repo configurations. ListSuggestedRepositories(context.Context, *connect_go.Request[v1.ListSuggestedRepositoriesRequest]) (*connect_go.Response[v1.ListSuggestedRepositoriesResponse], error) } @@ -100,9 +108,17 @@ func (c *sCMServiceClient) ListSuggestedRepositories(ctx context.Context, req *c // SCMServiceHandler is an implementation of the gitpod.v1.SCMService service. type SCMServiceHandler interface { + // SearchSCMTokens allows clients to retrieve SCM tokens based on the + // specified host. SearchSCMTokens(context.Context, *connect_go.Request[v1.SearchSCMTokensRequest]) (*connect_go.Response[v1.SearchSCMTokensResponse], error) + // GuessTokenScopes allows clients to retrieve scopes their SCM token would + // require for the specified git command. GuessTokenScopes(context.Context, *connect_go.Request[v1.GuessTokenScopesRequest]) (*connect_go.Response[v1.GuessTokenScopesResponse], error) + // SearchRepositories allows clients to search for suggested repositories of + // SCM providers they are connected with. SearchRepositories(context.Context, *connect_go.Request[v1.SearchRepositoriesRequest]) (*connect_go.Response[v1.SearchRepositoriesResponse], error) + // ListSuggestedRepositories allows clients to list suggested repositories + // based on recent workspaces and accessible repo configurations. ListSuggestedRepositories(context.Context, *connect_go.Request[v1.ListSuggestedRepositoriesRequest]) (*connect_go.Response[v1.ListSuggestedRepositoriesResponse], error) } diff --git a/components/public-api/go/v1/v1connect/verification.connect.go b/components/public-api/go/v1/v1connect/verification.connect.go new file mode 100644 index 00000000000000..d1ddfa360ab6d3 --- /dev/null +++ b/components/public-api/go/v1/v1connect/verification.connect.go @@ -0,0 +1,122 @@ +// Copyright (c) 2023 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: gitpod/v1/verification.proto + +package v1connect + +import ( + context "context" + errors "errors" + connect_go "github.com/bufbuild/connect-go" + v1 "github.com/gitpod-io/gitpod/components/public-api/go/v1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect_go.IsAtLeastVersion0_1_0 + +const ( + // VerificationServiceName is the fully-qualified name of the VerificationService service. + VerificationServiceName = "gitpod.v1.VerificationService" +) + +// VerificationServiceClient is a client for the gitpod.v1.VerificationService service. +type VerificationServiceClient interface { + // SendPhoneNumberVerificationToken sends a verification token to the + // specified phone number. + SendPhoneNumberVerificationToken(context.Context, *connect_go.Request[v1.SendPhoneNumberVerificationTokenRequest]) (*connect_go.Response[v1.SendPhoneNumberVerificationTokenResponse], error) + // VerifyPhoneNumberVerificationToken verifies the specified verification + // token. + VerifyPhoneNumberVerificationToken(context.Context, *connect_go.Request[v1.VerifyPhoneNumberVerificationTokenRequest]) (*connect_go.Response[v1.VerifyPhoneNumberVerificationTokenResponse], error) +} + +// NewVerificationServiceClient constructs a client for the gitpod.v1.VerificationService service. +// By default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped +// responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewVerificationServiceClient(httpClient connect_go.HTTPClient, baseURL string, opts ...connect_go.ClientOption) VerificationServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &verificationServiceClient{ + sendPhoneNumberVerificationToken: connect_go.NewClient[v1.SendPhoneNumberVerificationTokenRequest, v1.SendPhoneNumberVerificationTokenResponse]( + httpClient, + baseURL+"/gitpod.v1.VerificationService/SendPhoneNumberVerificationToken", + opts..., + ), + verifyPhoneNumberVerificationToken: connect_go.NewClient[v1.VerifyPhoneNumberVerificationTokenRequest, v1.VerifyPhoneNumberVerificationTokenResponse]( + httpClient, + baseURL+"/gitpod.v1.VerificationService/VerifyPhoneNumberVerificationToken", + opts..., + ), + } +} + +// verificationServiceClient implements VerificationServiceClient. +type verificationServiceClient struct { + sendPhoneNumberVerificationToken *connect_go.Client[v1.SendPhoneNumberVerificationTokenRequest, v1.SendPhoneNumberVerificationTokenResponse] + verifyPhoneNumberVerificationToken *connect_go.Client[v1.VerifyPhoneNumberVerificationTokenRequest, v1.VerifyPhoneNumberVerificationTokenResponse] +} + +// SendPhoneNumberVerificationToken calls +// gitpod.v1.VerificationService.SendPhoneNumberVerificationToken. +func (c *verificationServiceClient) SendPhoneNumberVerificationToken(ctx context.Context, req *connect_go.Request[v1.SendPhoneNumberVerificationTokenRequest]) (*connect_go.Response[v1.SendPhoneNumberVerificationTokenResponse], error) { + return c.sendPhoneNumberVerificationToken.CallUnary(ctx, req) +} + +// VerifyPhoneNumberVerificationToken calls +// gitpod.v1.VerificationService.VerifyPhoneNumberVerificationToken. +func (c *verificationServiceClient) VerifyPhoneNumberVerificationToken(ctx context.Context, req *connect_go.Request[v1.VerifyPhoneNumberVerificationTokenRequest]) (*connect_go.Response[v1.VerifyPhoneNumberVerificationTokenResponse], error) { + return c.verifyPhoneNumberVerificationToken.CallUnary(ctx, req) +} + +// VerificationServiceHandler is an implementation of the gitpod.v1.VerificationService service. +type VerificationServiceHandler interface { + // SendPhoneNumberVerificationToken sends a verification token to the + // specified phone number. + SendPhoneNumberVerificationToken(context.Context, *connect_go.Request[v1.SendPhoneNumberVerificationTokenRequest]) (*connect_go.Response[v1.SendPhoneNumberVerificationTokenResponse], error) + // VerifyPhoneNumberVerificationToken verifies the specified verification + // token. + VerifyPhoneNumberVerificationToken(context.Context, *connect_go.Request[v1.VerifyPhoneNumberVerificationTokenRequest]) (*connect_go.Response[v1.VerifyPhoneNumberVerificationTokenResponse], error) +} + +// NewVerificationServiceHandler builds an HTTP handler from the service implementation. It returns +// the path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewVerificationServiceHandler(svc VerificationServiceHandler, opts ...connect_go.HandlerOption) (string, http.Handler) { + mux := http.NewServeMux() + mux.Handle("/gitpod.v1.VerificationService/SendPhoneNumberVerificationToken", connect_go.NewUnaryHandler( + "/gitpod.v1.VerificationService/SendPhoneNumberVerificationToken", + svc.SendPhoneNumberVerificationToken, + opts..., + )) + mux.Handle("/gitpod.v1.VerificationService/VerifyPhoneNumberVerificationToken", connect_go.NewUnaryHandler( + "/gitpod.v1.VerificationService/VerifyPhoneNumberVerificationToken", + svc.VerifyPhoneNumberVerificationToken, + opts..., + )) + return "/gitpod.v1.VerificationService/", mux +} + +// UnimplementedVerificationServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedVerificationServiceHandler struct{} + +func (UnimplementedVerificationServiceHandler) SendPhoneNumberVerificationToken(context.Context, *connect_go.Request[v1.SendPhoneNumberVerificationTokenRequest]) (*connect_go.Response[v1.SendPhoneNumberVerificationTokenResponse], error) { + return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("gitpod.v1.VerificationService.SendPhoneNumberVerificationToken is not implemented")) +} + +func (UnimplementedVerificationServiceHandler) VerifyPhoneNumberVerificationToken(context.Context, *connect_go.Request[v1.VerifyPhoneNumberVerificationTokenRequest]) (*connect_go.Response[v1.VerifyPhoneNumberVerificationTokenResponse], error) { + return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("gitpod.v1.VerificationService.VerifyPhoneNumberVerificationToken is not implemented")) +} diff --git a/components/public-api/go/v1/v1connect/verification.proxy.connect.go b/components/public-api/go/v1/v1connect/verification.proxy.connect.go new file mode 100644 index 00000000000000..25473e044e5012 --- /dev/null +++ b/components/public-api/go/v1/v1connect/verification.proxy.connect.go @@ -0,0 +1,40 @@ +// Copyright (c) 2023 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +// Code generated by protoc-proxy-gen. DO NOT EDIT. + +package v1connect + +import ( + context "context" + connect_go "github.com/bufbuild/connect-go" + v1 "github.com/gitpod-io/gitpod/components/public-api/go/v1" +) + +var _ VerificationServiceHandler = (*ProxyVerificationServiceHandler)(nil) + +type ProxyVerificationServiceHandler struct { + Client v1.VerificationServiceClient + UnimplementedVerificationServiceHandler +} + +func (s *ProxyVerificationServiceHandler) SendPhoneNumberVerificationToken(ctx context.Context, req *connect_go.Request[v1.SendPhoneNumberVerificationTokenRequest]) (*connect_go.Response[v1.SendPhoneNumberVerificationTokenResponse], error) { + resp, err := s.Client.SendPhoneNumberVerificationToken(ctx, req.Msg) + if err != nil { + // TODO(milan): Convert to correct status code + return nil, err + } + + return connect_go.NewResponse(resp), nil +} + +func (s *ProxyVerificationServiceHandler) VerifyPhoneNumberVerificationToken(ctx context.Context, req *connect_go.Request[v1.VerifyPhoneNumberVerificationTokenRequest]) (*connect_go.Response[v1.VerifyPhoneNumberVerificationTokenResponse], error) { + resp, err := s.Client.VerifyPhoneNumberVerificationToken(ctx, req.Msg) + if err != nil { + // TODO(milan): Convert to correct status code + return nil, err + } + + return connect_go.NewResponse(resp), nil +} diff --git a/components/public-api/go/v1/verification.pb.go b/components/public-api/go/v1/verification.pb.go new file mode 100644 index 00000000000000..f951db3a04dfb1 --- /dev/null +++ b/components/public-api/go/v1/verification.pb.go @@ -0,0 +1,403 @@ +// Copyright (c) 2023 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: gitpod/v1/verification.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Required fields: +// - phone_number +type SendPhoneNumberVerificationTokenRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // phone_number in E.164 format + PhoneNumber string `protobuf:"bytes,1,opt,name=phone_number,json=phoneNumber,proto3" json:"phone_number,omitempty"` +} + +func (x *SendPhoneNumberVerificationTokenRequest) Reset() { + *x = SendPhoneNumberVerificationTokenRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gitpod_v1_verification_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SendPhoneNumberVerificationTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendPhoneNumberVerificationTokenRequest) ProtoMessage() {} + +func (x *SendPhoneNumberVerificationTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_gitpod_v1_verification_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendPhoneNumberVerificationTokenRequest.ProtoReflect.Descriptor instead. +func (*SendPhoneNumberVerificationTokenRequest) Descriptor() ([]byte, []int) { + return file_gitpod_v1_verification_proto_rawDescGZIP(), []int{0} +} + +func (x *SendPhoneNumberVerificationTokenRequest) GetPhoneNumber() string { + if x != nil { + return x.PhoneNumber + } + return "" +} + +type SendPhoneNumberVerificationTokenResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // verification_id is used to VerifyPhoneNumberVerificationToken + VerificationId string `protobuf:"bytes,1,opt,name=verification_id,json=verificationId,proto3" json:"verification_id,omitempty"` +} + +func (x *SendPhoneNumberVerificationTokenResponse) Reset() { + *x = SendPhoneNumberVerificationTokenResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_gitpod_v1_verification_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SendPhoneNumberVerificationTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendPhoneNumberVerificationTokenResponse) ProtoMessage() {} + +func (x *SendPhoneNumberVerificationTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_gitpod_v1_verification_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendPhoneNumberVerificationTokenResponse.ProtoReflect.Descriptor instead. +func (*SendPhoneNumberVerificationTokenResponse) Descriptor() ([]byte, []int) { + return file_gitpod_v1_verification_proto_rawDescGZIP(), []int{1} +} + +func (x *SendPhoneNumberVerificationTokenResponse) GetVerificationId() string { + if x != nil { + return x.VerificationId + } + return "" +} + +// Required fields: +// - phone_number +// - verification_id +// - token +type VerifyPhoneNumberVerificationTokenRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // phone_number in E.164 format + PhoneNumber string `protobuf:"bytes,1,opt,name=phone_number,json=phoneNumber,proto3" json:"phone_number,omitempty"` + // verification_id is returned by SendPhoneNumberVerificationToken + VerificationId string `protobuf:"bytes,2,opt,name=verification_id,json=verificationId,proto3" json:"verification_id,omitempty"` + // token is the verification token from providers + Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` +} + +func (x *VerifyPhoneNumberVerificationTokenRequest) Reset() { + *x = VerifyPhoneNumberVerificationTokenRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gitpod_v1_verification_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VerifyPhoneNumberVerificationTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerifyPhoneNumberVerificationTokenRequest) ProtoMessage() {} + +func (x *VerifyPhoneNumberVerificationTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_gitpod_v1_verification_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VerifyPhoneNumberVerificationTokenRequest.ProtoReflect.Descriptor instead. +func (*VerifyPhoneNumberVerificationTokenRequest) Descriptor() ([]byte, []int) { + return file_gitpod_v1_verification_proto_rawDescGZIP(), []int{2} +} + +func (x *VerifyPhoneNumberVerificationTokenRequest) GetPhoneNumber() string { + if x != nil { + return x.PhoneNumber + } + return "" +} + +func (x *VerifyPhoneNumberVerificationTokenRequest) GetVerificationId() string { + if x != nil { + return x.VerificationId + } + return "" +} + +func (x *VerifyPhoneNumberVerificationTokenRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +type VerifyPhoneNumberVerificationTokenResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // verified indicates if the verification was successful + Verified bool `protobuf:"varint,1,opt,name=verified,proto3" json:"verified,omitempty"` +} + +func (x *VerifyPhoneNumberVerificationTokenResponse) Reset() { + *x = VerifyPhoneNumberVerificationTokenResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_gitpod_v1_verification_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VerifyPhoneNumberVerificationTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerifyPhoneNumberVerificationTokenResponse) ProtoMessage() {} + +func (x *VerifyPhoneNumberVerificationTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_gitpod_v1_verification_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VerifyPhoneNumberVerificationTokenResponse.ProtoReflect.Descriptor instead. +func (*VerifyPhoneNumberVerificationTokenResponse) Descriptor() ([]byte, []int) { + return file_gitpod_v1_verification_proto_rawDescGZIP(), []int{3} +} + +func (x *VerifyPhoneNumberVerificationTokenResponse) GetVerified() bool { + if x != nil { + return x.Verified + } + return false +} + +var File_gitpod_v1_verification_proto protoreflect.FileDescriptor + +var file_gitpod_v1_verification_proto_rawDesc = []byte{ + 0x0a, 0x1c, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2f, 0x76, 0x31, 0x2f, 0x76, 0x65, 0x72, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, + 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x22, 0x4c, 0x0a, 0x27, 0x53, 0x65, 0x6e, + 0x64, 0x50, 0x68, 0x6f, 0x6e, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x56, 0x65, 0x72, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x5f, 0x6e, 0x75, + 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x68, 0x6f, 0x6e, + 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x53, 0x0a, 0x28, 0x53, 0x65, 0x6e, 0x64, 0x50, + 0x68, 0x6f, 0x6e, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x76, 0x65, + 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x8d, 0x01, 0x0a, + 0x29, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x68, 0x6f, 0x6e, 0x65, 0x4e, 0x75, 0x6d, 0x62, + 0x65, 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x68, + 0x6f, 0x6e, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x27, 0x0a, + 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x48, 0x0a, 0x2a, + 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x68, 0x6f, 0x6e, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, + 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x65, + 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x76, 0x65, + 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x32, 0xbb, 0x02, 0x0a, 0x13, 0x56, 0x65, 0x72, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x8d, + 0x01, 0x0a, 0x20, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x68, 0x6f, 0x6e, 0x65, 0x4e, 0x75, 0x6d, 0x62, + 0x65, 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x32, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x65, 0x6e, 0x64, 0x50, 0x68, 0x6f, 0x6e, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x56, + 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x50, 0x68, 0x6f, 0x6e, 0x65, 0x4e, 0x75, 0x6d, + 0x62, 0x65, 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x93, + 0x01, 0x0a, 0x22, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x68, 0x6f, 0x6e, 0x65, 0x4e, 0x75, + 0x6d, 0x62, 0x65, 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x2e, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, + 0x31, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x68, 0x6f, 0x6e, 0x65, 0x4e, 0x75, 0x6d, + 0x62, 0x65, 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x67, 0x69, + 0x74, 0x70, 0x6f, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x68, + 0x6f, 0x6e, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x67, 0x69, 0x74, 0x70, 0x6f, 0x64, 0x2d, 0x69, 0x6f, 0x2f, 0x67, 0x69, 0x74, + 0x70, 0x6f, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x70, + 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2d, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_gitpod_v1_verification_proto_rawDescOnce sync.Once + file_gitpod_v1_verification_proto_rawDescData = file_gitpod_v1_verification_proto_rawDesc +) + +func file_gitpod_v1_verification_proto_rawDescGZIP() []byte { + file_gitpod_v1_verification_proto_rawDescOnce.Do(func() { + file_gitpod_v1_verification_proto_rawDescData = protoimpl.X.CompressGZIP(file_gitpod_v1_verification_proto_rawDescData) + }) + return file_gitpod_v1_verification_proto_rawDescData +} + +var file_gitpod_v1_verification_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_gitpod_v1_verification_proto_goTypes = []interface{}{ + (*SendPhoneNumberVerificationTokenRequest)(nil), // 0: gitpod.v1.SendPhoneNumberVerificationTokenRequest + (*SendPhoneNumberVerificationTokenResponse)(nil), // 1: gitpod.v1.SendPhoneNumberVerificationTokenResponse + (*VerifyPhoneNumberVerificationTokenRequest)(nil), // 2: gitpod.v1.VerifyPhoneNumberVerificationTokenRequest + (*VerifyPhoneNumberVerificationTokenResponse)(nil), // 3: gitpod.v1.VerifyPhoneNumberVerificationTokenResponse +} +var file_gitpod_v1_verification_proto_depIdxs = []int32{ + 0, // 0: gitpod.v1.VerificationService.SendPhoneNumberVerificationToken:input_type -> gitpod.v1.SendPhoneNumberVerificationTokenRequest + 2, // 1: gitpod.v1.VerificationService.VerifyPhoneNumberVerificationToken:input_type -> gitpod.v1.VerifyPhoneNumberVerificationTokenRequest + 1, // 2: gitpod.v1.VerificationService.SendPhoneNumberVerificationToken:output_type -> gitpod.v1.SendPhoneNumberVerificationTokenResponse + 3, // 3: gitpod.v1.VerificationService.VerifyPhoneNumberVerificationToken:output_type -> gitpod.v1.VerifyPhoneNumberVerificationTokenResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_gitpod_v1_verification_proto_init() } +func file_gitpod_v1_verification_proto_init() { + if File_gitpod_v1_verification_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_gitpod_v1_verification_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SendPhoneNumberVerificationTokenRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gitpod_v1_verification_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SendPhoneNumberVerificationTokenResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gitpod_v1_verification_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VerifyPhoneNumberVerificationTokenRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gitpod_v1_verification_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VerifyPhoneNumberVerificationTokenResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_gitpod_v1_verification_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_gitpod_v1_verification_proto_goTypes, + DependencyIndexes: file_gitpod_v1_verification_proto_depIdxs, + MessageInfos: file_gitpod_v1_verification_proto_msgTypes, + }.Build() + File_gitpod_v1_verification_proto = out.File + file_gitpod_v1_verification_proto_rawDesc = nil + file_gitpod_v1_verification_proto_goTypes = nil + file_gitpod_v1_verification_proto_depIdxs = nil +} diff --git a/components/public-api/go/v1/verification_grpc.pb.go b/components/public-api/go/v1/verification_grpc.pb.go new file mode 100644 index 00000000000000..cccea279d5d455 --- /dev/null +++ b/components/public-api/go/v1/verification_grpc.pb.go @@ -0,0 +1,153 @@ +// Copyright (c) 2023 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc (unknown) +// source: gitpod/v1/verification.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// VerificationServiceClient is the client API for VerificationService service. +// +// 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 VerificationServiceClient interface { + // SendPhoneNumberVerificationToken sends a verification token to the + // specified phone number. + SendPhoneNumberVerificationToken(ctx context.Context, in *SendPhoneNumberVerificationTokenRequest, opts ...grpc.CallOption) (*SendPhoneNumberVerificationTokenResponse, error) + // VerifyPhoneNumberVerificationToken verifies the specified verification + // token. + VerifyPhoneNumberVerificationToken(ctx context.Context, in *VerifyPhoneNumberVerificationTokenRequest, opts ...grpc.CallOption) (*VerifyPhoneNumberVerificationTokenResponse, error) +} + +type verificationServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewVerificationServiceClient(cc grpc.ClientConnInterface) VerificationServiceClient { + return &verificationServiceClient{cc} +} + +func (c *verificationServiceClient) SendPhoneNumberVerificationToken(ctx context.Context, in *SendPhoneNumberVerificationTokenRequest, opts ...grpc.CallOption) (*SendPhoneNumberVerificationTokenResponse, error) { + out := new(SendPhoneNumberVerificationTokenResponse) + err := c.cc.Invoke(ctx, "/gitpod.v1.VerificationService/SendPhoneNumberVerificationToken", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *verificationServiceClient) VerifyPhoneNumberVerificationToken(ctx context.Context, in *VerifyPhoneNumberVerificationTokenRequest, opts ...grpc.CallOption) (*VerifyPhoneNumberVerificationTokenResponse, error) { + out := new(VerifyPhoneNumberVerificationTokenResponse) + err := c.cc.Invoke(ctx, "/gitpod.v1.VerificationService/VerifyPhoneNumberVerificationToken", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// VerificationServiceServer is the server API for VerificationService service. +// All implementations must embed UnimplementedVerificationServiceServer +// for forward compatibility +type VerificationServiceServer interface { + // SendPhoneNumberVerificationToken sends a verification token to the + // specified phone number. + SendPhoneNumberVerificationToken(context.Context, *SendPhoneNumberVerificationTokenRequest) (*SendPhoneNumberVerificationTokenResponse, error) + // VerifyPhoneNumberVerificationToken verifies the specified verification + // token. + VerifyPhoneNumberVerificationToken(context.Context, *VerifyPhoneNumberVerificationTokenRequest) (*VerifyPhoneNumberVerificationTokenResponse, error) + mustEmbedUnimplementedVerificationServiceServer() +} + +// UnimplementedVerificationServiceServer must be embedded to have forward compatible implementations. +type UnimplementedVerificationServiceServer struct { +} + +func (UnimplementedVerificationServiceServer) SendPhoneNumberVerificationToken(context.Context, *SendPhoneNumberVerificationTokenRequest) (*SendPhoneNumberVerificationTokenResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SendPhoneNumberVerificationToken not implemented") +} +func (UnimplementedVerificationServiceServer) VerifyPhoneNumberVerificationToken(context.Context, *VerifyPhoneNumberVerificationTokenRequest) (*VerifyPhoneNumberVerificationTokenResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method VerifyPhoneNumberVerificationToken not implemented") +} +func (UnimplementedVerificationServiceServer) mustEmbedUnimplementedVerificationServiceServer() {} + +// UnsafeVerificationServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to VerificationServiceServer will +// result in compilation errors. +type UnsafeVerificationServiceServer interface { + mustEmbedUnimplementedVerificationServiceServer() +} + +func RegisterVerificationServiceServer(s grpc.ServiceRegistrar, srv VerificationServiceServer) { + s.RegisterService(&VerificationService_ServiceDesc, srv) +} + +func _VerificationService_SendPhoneNumberVerificationToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SendPhoneNumberVerificationTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VerificationServiceServer).SendPhoneNumberVerificationToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gitpod.v1.VerificationService/SendPhoneNumberVerificationToken", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VerificationServiceServer).SendPhoneNumberVerificationToken(ctx, req.(*SendPhoneNumberVerificationTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _VerificationService_VerifyPhoneNumberVerificationToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VerifyPhoneNumberVerificationTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VerificationServiceServer).VerifyPhoneNumberVerificationToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gitpod.v1.VerificationService/VerifyPhoneNumberVerificationToken", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VerificationServiceServer).VerifyPhoneNumberVerificationToken(ctx, req.(*VerifyPhoneNumberVerificationTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// VerificationService_ServiceDesc is the grpc.ServiceDesc for VerificationService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var VerificationService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "gitpod.v1.VerificationService", + HandlerType: (*VerificationServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SendPhoneNumberVerificationToken", + Handler: _VerificationService_SendPhoneNumberVerificationToken_Handler, + }, + { + MethodName: "VerifyPhoneNumberVerificationToken", + Handler: _VerificationService_VerifyPhoneNumberVerificationToken_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "gitpod/v1/verification.proto", +} diff --git a/components/public-api/typescript/src/gitpod/v1/scm_connect.ts b/components/public-api/typescript/src/gitpod/v1/scm_connect.ts index 451ff7e341033a..d3aa713b514fe8 100644 --- a/components/public-api/typescript/src/gitpod/v1/scm_connect.ts +++ b/components/public-api/typescript/src/gitpod/v1/scm_connect.ts @@ -19,6 +19,9 @@ export const SCMService = { typeName: "gitpod.v1.SCMService", methods: { /** + * SearchSCMTokens allows clients to retrieve SCM tokens based on the + * specified host. + * * @generated from rpc gitpod.v1.SCMService.SearchSCMTokens */ searchSCMTokens: { @@ -28,6 +31,9 @@ export const SCMService = { kind: MethodKind.Unary, }, /** + * GuessTokenScopes allows clients to retrieve scopes their SCM token would + * require for the specified git command. + * * @generated from rpc gitpod.v1.SCMService.GuessTokenScopes */ guessTokenScopes: { @@ -37,6 +43,9 @@ export const SCMService = { kind: MethodKind.Unary, }, /** + * SearchRepositories allows clients to search for suggested repositories of + * SCM providers they are connected with. + * * @generated from rpc gitpod.v1.SCMService.SearchRepositories */ searchRepositories: { @@ -46,6 +55,9 @@ export const SCMService = { kind: MethodKind.Unary, }, /** + * ListSuggestedRepositories allows clients to list suggested repositories + * based on recent workspaces and accessible repo configurations. + * * @generated from rpc gitpod.v1.SCMService.ListSuggestedRepositories */ listSuggestedRepositories: { diff --git a/components/public-api/typescript/src/gitpod/v1/verification_connect.ts b/components/public-api/typescript/src/gitpod/v1/verification_connect.ts new file mode 100644 index 00000000000000..7899904ebcaca5 --- /dev/null +++ b/components/public-api/typescript/src/gitpod/v1/verification_connect.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2023 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +// @generated by protoc-gen-connect-es v1.1.2 with parameter "target=ts" +// @generated from file gitpod/v1/verification.proto (package gitpod.v1, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { SendPhoneNumberVerificationTokenRequest, SendPhoneNumberVerificationTokenResponse, VerifyPhoneNumberVerificationTokenRequest, VerifyPhoneNumberVerificationTokenResponse } from "./verification_pb.js"; +import { MethodKind } from "@bufbuild/protobuf"; + +/** + * @generated from service gitpod.v1.VerificationService + */ +export const VerificationService = { + typeName: "gitpod.v1.VerificationService", + methods: { + /** + * SendPhoneNumberVerificationToken sends a verification token to the + * specified phone number. + * + * @generated from rpc gitpod.v1.VerificationService.SendPhoneNumberVerificationToken + */ + sendPhoneNumberVerificationToken: { + name: "SendPhoneNumberVerificationToken", + I: SendPhoneNumberVerificationTokenRequest, + O: SendPhoneNumberVerificationTokenResponse, + kind: MethodKind.Unary, + }, + /** + * VerifyPhoneNumberVerificationToken verifies the specified verification + * token. + * + * @generated from rpc gitpod.v1.VerificationService.VerifyPhoneNumberVerificationToken + */ + verifyPhoneNumberVerificationToken: { + name: "VerifyPhoneNumberVerificationToken", + I: VerifyPhoneNumberVerificationTokenRequest, + O: VerifyPhoneNumberVerificationTokenResponse, + kind: MethodKind.Unary, + }, + } +} as const; diff --git a/components/public-api/typescript/src/gitpod/v1/verification_pb.ts b/components/public-api/typescript/src/gitpod/v1/verification_pb.ts new file mode 100644 index 00000000000000..ed3b664a10aaef --- /dev/null +++ b/components/public-api/typescript/src/gitpod/v1/verification_pb.ts @@ -0,0 +1,193 @@ +/** + * Copyright (c) 2023 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +// @generated by protoc-gen-es v1.3.3 with parameter "target=ts" +// @generated from file gitpod/v1/verification.proto (package gitpod.v1, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; +import { Message, proto3 } from "@bufbuild/protobuf"; + +/** + * Required fields: + * - phone_number + * + * @generated from message gitpod.v1.SendPhoneNumberVerificationTokenRequest + */ +export class SendPhoneNumberVerificationTokenRequest extends Message { + /** + * phone_number in E.164 format + * + * @generated from field: string phone_number = 1; + */ + phoneNumber = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "gitpod.v1.SendPhoneNumberVerificationTokenRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "phone_number", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): SendPhoneNumberVerificationTokenRequest { + return new SendPhoneNumberVerificationTokenRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): SendPhoneNumberVerificationTokenRequest { + return new SendPhoneNumberVerificationTokenRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): SendPhoneNumberVerificationTokenRequest { + return new SendPhoneNumberVerificationTokenRequest().fromJsonString(jsonString, options); + } + + static equals(a: SendPhoneNumberVerificationTokenRequest | PlainMessage | undefined, b: SendPhoneNumberVerificationTokenRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(SendPhoneNumberVerificationTokenRequest, a, b); + } +} + +/** + * @generated from message gitpod.v1.SendPhoneNumberVerificationTokenResponse + */ +export class SendPhoneNumberVerificationTokenResponse extends Message { + /** + * verification_id is used to VerifyPhoneNumberVerificationToken + * + * @generated from field: string verification_id = 1; + */ + verificationId = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "gitpod.v1.SendPhoneNumberVerificationTokenResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "verification_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): SendPhoneNumberVerificationTokenResponse { + return new SendPhoneNumberVerificationTokenResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): SendPhoneNumberVerificationTokenResponse { + return new SendPhoneNumberVerificationTokenResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): SendPhoneNumberVerificationTokenResponse { + return new SendPhoneNumberVerificationTokenResponse().fromJsonString(jsonString, options); + } + + static equals(a: SendPhoneNumberVerificationTokenResponse | PlainMessage | undefined, b: SendPhoneNumberVerificationTokenResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(SendPhoneNumberVerificationTokenResponse, a, b); + } +} + +/** + * Required fields: + * - phone_number + * - verification_id + * - token + * + * @generated from message gitpod.v1.VerifyPhoneNumberVerificationTokenRequest + */ +export class VerifyPhoneNumberVerificationTokenRequest extends Message { + /** + * phone_number in E.164 format + * + * @generated from field: string phone_number = 1; + */ + phoneNumber = ""; + + /** + * verification_id is returned by SendPhoneNumberVerificationToken + * + * @generated from field: string verification_id = 2; + */ + verificationId = ""; + + /** + * token is the verification token from providers + * + * @generated from field: string token = 3; + */ + token = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "gitpod.v1.VerifyPhoneNumberVerificationTokenRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "phone_number", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "verification_id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "token", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): VerifyPhoneNumberVerificationTokenRequest { + return new VerifyPhoneNumberVerificationTokenRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): VerifyPhoneNumberVerificationTokenRequest { + return new VerifyPhoneNumberVerificationTokenRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): VerifyPhoneNumberVerificationTokenRequest { + return new VerifyPhoneNumberVerificationTokenRequest().fromJsonString(jsonString, options); + } + + static equals(a: VerifyPhoneNumberVerificationTokenRequest | PlainMessage | undefined, b: VerifyPhoneNumberVerificationTokenRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(VerifyPhoneNumberVerificationTokenRequest, a, b); + } +} + +/** + * @generated from message gitpod.v1.VerifyPhoneNumberVerificationTokenResponse + */ +export class VerifyPhoneNumberVerificationTokenResponse extends Message { + /** + * verified indicates if the verification was successful + * + * @generated from field: bool verified = 1; + */ + verified = false; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "gitpod.v1.VerifyPhoneNumberVerificationTokenResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "verified", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): VerifyPhoneNumberVerificationTokenResponse { + return new VerifyPhoneNumberVerificationTokenResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): VerifyPhoneNumberVerificationTokenResponse { + return new VerifyPhoneNumberVerificationTokenResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): VerifyPhoneNumberVerificationTokenResponse { + return new VerifyPhoneNumberVerificationTokenResponse().fromJsonString(jsonString, options); + } + + static equals(a: VerifyPhoneNumberVerificationTokenResponse | PlainMessage | undefined, b: VerifyPhoneNumberVerificationTokenResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(VerifyPhoneNumberVerificationTokenResponse, a, b); + } +} diff --git a/components/server/src/api/verification-service-api.ts b/components/server/src/api/verification-service-api.ts new file mode 100644 index 00000000000000..1f88b6044d56ce --- /dev/null +++ b/components/server/src/api/verification-service-api.ts @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2023 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { HandlerContext, ServiceImpl } from "@connectrpc/connect"; +import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; +import { VerificationService as VerificationServiceInterface } from "@gitpod/public-api/lib/gitpod/v1/verification_connect"; +import { + SendPhoneNumberVerificationTokenRequest, + SendPhoneNumberVerificationTokenResponse, + VerifyPhoneNumberVerificationTokenRequest, + VerifyPhoneNumberVerificationTokenResponse, +} from "@gitpod/public-api/lib/gitpod/v1/verification_pb"; +import { inject, injectable } from "inversify"; +import { VerificationService } from "../auth/verification-service"; +import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; +import { ctxUserId } from "../util/request-context"; +import { UserService } from "../user/user-service"; +import { formatPhoneNumber } from "../user/phone-numbers"; +import { validate as uuidValidate } from "uuid"; + +@injectable() +export class VerificationServiceAPI implements ServiceImpl { + @inject(VerificationService) private readonly verificationService: VerificationService; + @inject(UserService) private readonly userService: UserService; + + async sendPhoneNumberVerificationToken( + req: SendPhoneNumberVerificationTokenRequest, + _: HandlerContext, + ): Promise { + if (!req.phoneNumber) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "phoneNumber is required"); + } + + const userId = ctxUserId(); + const user = await this.userService.findUserById(userId, userId); + + // Check if verify via call is enabled + const phoneVerificationByCall = await getExperimentsClientForBackend().getValueAsync( + "phoneVerificationByCall", + false, + { + user, + }, + ); + const channel = phoneVerificationByCall ? "call" : "sms"; + const verificationId = await this.verificationService.sendVerificationToken( + formatPhoneNumber(req.phoneNumber), + channel, + ); + return new SendPhoneNumberVerificationTokenResponse({ + verificationId, + }); + } + + async verifyPhoneNumberVerificationToken( + req: VerifyPhoneNumberVerificationTokenRequest, + _: HandlerContext, + ): Promise { + if (!req.phoneNumber) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "phoneNumber is required"); + } + if (!req.verificationId || !uuidValidate(req.verificationId)) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "verificationId is required"); + } + if (!req.token) { + throw new ApplicationError(ErrorCodes.BAD_REQUEST, "token is required"); + } + const phoneNumber = formatPhoneNumber(req.phoneNumber); + + const userId = ctxUserId(); + const user = await this.userService.findUserById(userId, userId); + + const verified = await this.verificationService.verifyVerificationToken( + user, + phoneNumber, + req.token, + req.verificationId, + ); + return new VerifyPhoneNumberVerificationTokenResponse({ + verified, + }); + } +} diff --git a/components/server/src/auth/generic-auth-provider.ts b/components/server/src/auth/generic-auth-provider.ts index 30270b95b89ce2..bff07858b431a6 100644 --- a/components/server/src/auth/generic-auth-provider.ts +++ b/components/server/src/auth/generic-auth-provider.ts @@ -489,7 +489,7 @@ export abstract class GenericAuthProvider implements AuthProvider { isDateSmaller(authUser.created_at, daysBefore(new Date().toISOString(), 30)) ) { // people with an account older than 30 days are treated as trusted - this.verificationService.markVerified(newUser); + newUser.lastVerificationTime = new Date().toISOString(); } }, }); diff --git a/components/server/src/auth/verification-service.ts b/components/server/src/auth/verification-service.ts index f7c66d7211647a..fefcd70c99747b 100644 --- a/components/server/src/auth/verification-service.ts +++ b/components/server/src/auth/verification-service.ts @@ -6,35 +6,121 @@ import { User } from "@gitpod/gitpod-protocol"; import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; -import { inject, injectable, postConstruct } from "inversify"; +import { inject, injectable } from "inversify"; import { Config } from "../config"; import { Twilio } from "twilio"; import { ServiceContext } from "twilio/lib/rest/verify/v2/service"; -import { TeamDB, UserDB, WorkspaceDB } from "@gitpod/gitpod-db/lib"; +import { TeamDB, UserDB } from "@gitpod/gitpod-db/lib"; import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error"; -import { VerificationInstance } from "twilio/lib/rest/verify/v2/service/verification"; import { v4 as uuidv4, validate as uuidValidate } from "uuid"; import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; +import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics"; +import { UserService } from "../user/user-service"; -@injectable() -export class VerificationService { - @inject(Config) protected config: Config; - @inject(WorkspaceDB) protected workspaceDB: WorkspaceDB; - @inject(UserDB) protected userDB: UserDB; - @inject(TeamDB) protected teamDB: TeamDB; +interface VerificationEndpoint { + sendToken(phoneNumber: string, channel: "sms" | "call"): Promise; + verifyToken(phoneNumber: string, oneTimePassword: string, verificationId: string): Promise; +} - protected verifyService: ServiceContext; +class TwilioVerificationEndpoint implements VerificationEndpoint { + constructor(private readonly config: Config) {} - @postConstruct() - protected initialize(): void { - if (this.config.twilioConfig) { + private _twilioService: ServiceContext; + private get twilioService(): ServiceContext { + if (!this._twilioService && this.config.twilioConfig) { const client = new Twilio(this.config.twilioConfig.accountSID, this.config.twilioConfig.authToken); - this.verifyService = client.verify.v2.services(this.config.twilioConfig.serviceID); + this._twilioService = client.verify.v2.services(this.config.twilioConfig.serviceID); + } + return this._twilioService; + } + + public async sendToken(phoneNumber: string, channel: "sms" | "call"): Promise { + if (!this.twilioService) { + throw new Error("No verification service configured."); + } + const verification = await this.twilioService.verifications.create({ to: phoneNumber, channel }); + + // Create a unique id to correlate starting/completing of verification flow + // Clients receive this and send it back when they call send the verification code + const verificationId = uuidv4(); + + log.info("Verification code sent", { + verificationId, + phoneNumber, + status: verification.status, + // actual channel verification was created on + channel: verification.channel, + // channel we requested - these could differ if a channel is not enabled in a specific country + requestedChannel: channel, + }); + + // Help us identify if verification codes are not able to send via requested channel + if (channel !== verification.channel) { + log.info("Verification code sent via different channel than system requested", { + verificationId, + phoneNumber, + status: verification.status, + // actual channel verification was created on + channel: verification.channel, + // channel we requested - these could differ if a channel is not enabled in a specific country + requestedChannel: channel, + }); + } + return verificationId; + } + public async verifyToken(phoneNumber: string, oneTimePassword: string, verificationId: string): Promise { + const verification_check = await this.twilioService.verificationChecks.create({ + to: phoneNumber, + code: oneTimePassword, + }); + + log.info("Verification code checked", { + verificationId, + phoneNumber, + status: verification_check.status, + channel: verification_check.channel, + }); + + return verification_check.status === "approved"; + } +} + +class MockVerificationEndpoint implements VerificationEndpoint { + private verificationId = uuidv4(); + + public async sendToken(phoneNumber: string, channel: "sms" | "call"): Promise { + return this.verificationId; + } + + public async verifyToken(phoneNumber: string, oneTimePassword: string, verificationId: string): Promise { + if (verificationId !== this.verificationId) { + return false; } + return oneTimePassword === "123456"; } +} + +@injectable() +export class VerificationService { + constructor( + @inject(Config) private config: Config, + @inject(UserDB) private userDB: UserDB, + @inject(TeamDB) private teamDB: TeamDB, + @inject(IAnalyticsWriter) private readonly analytics: IAnalyticsWriter, + @inject(UserService) private readonly userService: UserService, + ) { + if (this.config.twilioConfig) { + this.verifyService = new TwilioVerificationEndpoint(this.config); + } else if (this.config.devBranch && !this.config.isSingleOrgInstallation) { + // preview environments get the mock verification endpoint + this.verifyService = new MockVerificationEndpoint(); + } + } + + private verifyService?: VerificationEndpoint; public async needsVerification(user: User): Promise { - if (!this.config.twilioConfig) { + if (!this.verifyService) { return false; } if (!!user.lastVerificationTime) { @@ -54,35 +140,23 @@ export class VerificationService { return isPhoneVerificationEnabled; } - public markVerified(user: User): User { - user.lastVerificationTime = new Date().toISOString(); - return user; - } - public async verifyOrgMembers(organizationId: string): Promise { const members = await this.teamDB.findMembersByTeam(organizationId); for (const member of members) { const user = await this.userDB.findUserById(member.userId); - if (user) { - await this.verifyUser(user); + if (user && (await this.needsVerification(user))) { + await this.userService.markUserAsVerified(user, undefined); } } } - public async verifyUser(user: User): Promise { - if (await this.needsVerification(user)) { - user = await this.userDB.storeUser(this.markVerified(user)); - log.info("User verified", { userId: user.id }); - } - return user; - } - public async sendVerificationToken( + userId: string, phoneNumber: string, channel: "sms" | "call" = "sms", - ): Promise<{ verification: VerificationInstance; verificationId: string }> { + ): Promise { if (!this.verifyService) { - throw new Error("No verification service configured."); + throw new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, "No verification service configured."); } const isBlockedNumber = this.userDB.isBlockedPhoneNumber(phoneNumber); const usages = await this.userDB.countUsagesOfPhoneNumber(phoneNumber); @@ -95,43 +169,25 @@ export class VerificationService { if (await isBlockedNumber) { throw new ApplicationError(ErrorCodes.INVALID_VALUE, "The given phone number is blocked due to abuse."); } - const verification = await this.verifyService.verifications.create({ to: phoneNumber, channel }); - - // Create a unique id to correlate starting/completing of verification flow - // Clients receive this and send it back when they call send the verification code - const verificationId = uuidv4(); - - log.info("Verification code sent", { - verificationId, - phoneNumber, - status: verification.status, - // actual channel verification was created on - channel: verification.channel, - // channel we requested - these could differ if a channel is not enabled in a specific country - requestedChannel: channel, + const verificationId = this.verifyService.sendToken(phoneNumber, channel); + this.analytics.track({ + event: "phone_verification_sent", + userId, + properties: { + verification_id: verificationId, + requested_channel: channel, + }, }); - // Help us identify if verification codes are not able to send via requested channel - if (channel !== verification.channel) { - log.info("Verification code sent via different channel than system requested", { - verificationId, - phoneNumber, - status: verification.status, - // actual channel verification was created on - channel: verification.channel, - // channel we requested - these could differ if a channel is not enabled in a specific country - requestedChannel: channel, - }); - } - - return { verification, verificationId }; + return verificationId; } public async verifyVerificationToken( + user: User, phoneNumber: string, oneTimePassword: string, verificationId: string, - ): Promise<{ verified: boolean; channel: string }> { + ): Promise { if (!this.verifyService) { throw new Error("No verification service configured."); } @@ -139,21 +195,26 @@ export class VerificationService { throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Verification ID must be a valid UUID"); } - const verification_check = await this.verifyService.verificationChecks.create({ - to: phoneNumber, - code: oneTimePassword, - }); + const verified = await this.verifyService.verifyToken(phoneNumber, oneTimePassword, verificationId); - log.info("Verification code checked", { - verificationId, - phoneNumber, - status: verification_check.status, - channel: verification_check.channel, - }); - - return { - verified: verification_check.status === "approved", - channel: verification_check.channel, - }; + if (verified) { + await this.userService.markUserAsVerified(user, phoneNumber); + this.analytics.track({ + event: "phone_verification_completed", + userId: user.id, + properties: { + verification_id: verificationId, + }, + }); + } else { + this.analytics.track({ + event: "phone_verification_failed", + userId: user.id, + properties: { + verification_id: verificationId, + }, + }); + } + return verified; } } diff --git a/components/server/src/user/user-service.ts b/components/server/src/user/user-service.ts index 7ee6c97e757493..d83c917854c719 100644 --- a/components/server/src/user/user-service.ts +++ b/components/server/src/user/user-service.ts @@ -302,4 +302,13 @@ export class UserService { }, }); } + + public async markUserAsVerified(user: User, phoneNumber: string | undefined) { + user.lastVerificationTime = new Date().toISOString(); + if (phoneNumber) { + user.verificationPhoneNumber = phoneNumber; + } + await this.userDb.updateUserPartial(user); + log.info("User verified", { userId: user.id }); + } } diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 4b947b237bc6fc..7a579c6dfe79d4 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -474,19 +474,11 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const channel = phoneVerificationByCall ? "call" : "sms"; - const { verification, verificationId } = await this.verificationService.sendVerificationToken( + const verificationId = await this.verificationService.sendVerificationToken( + user.id, formatPhoneNumber(rawPhoneNumber), channel, ); - this.analytics.track({ - event: "phone_verification_sent", - userId: user.id, - properties: { - verification_id: verificationId, - channel: verification.channel, - requested_channel: channel, - }, - }); return { verificationId, @@ -502,34 +494,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const phoneNumber = formatPhoneNumber(rawPhoneNumber); const user = await this.checkUser("verifyPhoneNumberVerificationToken"); - const { verified, channel } = await this.verificationService.verifyVerificationToken( - phoneNumber, - token, - verificationId, - ); - if (!verified) { - this.analytics.track({ - event: "phone_verification_failed", - userId: user.id, - properties: { - channel, - verification_id: verificationId, - }, - }); - return false; - } - this.verificationService.markVerified(user); - user.verificationPhoneNumber = phoneNumber; - await this.userDB.updateUserPartial(user); - this.analytics.track({ - event: "phone_verification_completed", - userId: user.id, - properties: { - channel, - verification_id: verificationId, - }, - }); - return true; + return await this.verificationService.verifyVerificationToken(user, phoneNumber, token, verificationId); } public async getClientRegion(ctx: TraceContext): Promise { @@ -1558,7 +1523,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { AttributionId.render({ kind: "team", teamId: org.id }), )) !== undefined ) { - await this.verificationService.verifyUser(user); + await this.userService.markUserAsVerified(user, undefined); } } catch (e) { log.warn("Failed to verify new org member", e); @@ -1965,9 +1930,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { const admin = await this.guardAdminAccess("adminVerifyUser", { id: userId }, Permission.ADMIN_USERS); await this.auth.checkPermissionOnUser(admin.id, "admin_control", userId); const user = await this.userService.findUserById(admin.id, userId); - - this.verificationService.markVerified(user); - await this.userDB.updateUserPartial(user); + await this.userService.markUserAsVerified(user, undefined); return user; }