diff --git a/api/v2/common.go b/api/v2/common.go new file mode 100644 index 0000000..6167550 --- /dev/null +++ b/api/v2/common.go @@ -0,0 +1,17 @@ +package v2 + +import ( + apiv2pb "github.com/boojack/slash/proto/gen/api/v2" + "github.com/boojack/slash/store" +) + +func convertRowStatusFromStore(rowStatus store.RowStatus) apiv2pb.RowStatus { + switch rowStatus { + case store.Normal: + return apiv2pb.RowStatus_ACTIVE + case store.Archived: + return apiv2pb.RowStatus_ARCHIVED + default: + return apiv2pb.RowStatus_ROW_STATUS_UNSPECIFIED + } +} diff --git a/api/v2/jwt.go b/api/v2/jwt.go new file mode 100644 index 0000000..0153178 --- /dev/null +++ b/api/v2/jwt.go @@ -0,0 +1,193 @@ +package v2 + +import ( + "context" + "net/http" + "strconv" + "strings" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/pkg/errors" + "github.com/boojack/slash/api/auth" + "github.com/boojack/slash/store" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +var authenticationAllowlistMethods = map[string]bool{ + "/memos.api.v2.UserService/GetUser": true, +} + +// IsAuthenticationAllowed returns whether the method is exempted from authentication. +func IsAuthenticationAllowed(fullMethodName string) bool { + if strings.HasPrefix(fullMethodName, "/grpc.reflection") { + return true + } + return authenticationAllowlistMethods[fullMethodName] +} + +// ContextKey is the key type of context value. +type ContextKey int + +const ( + // The key name used to store user id in the context + // user id is extracted from the jwt token subject field. + UserIDContextKey ContextKey = iota +) + +// GRPCAuthInterceptor is the auth interceptor for gRPC server. +type GRPCAuthInterceptor struct { + store *store.Store + secret string +} + +// NewGRPCAuthInterceptor returns a new API auth interceptor. +func NewGRPCAuthInterceptor(store *store.Store, secret string) *GRPCAuthInterceptor { + return &GRPCAuthInterceptor{ + store: store, + secret: secret, + } +} + +// AuthenticationInterceptor is the unary interceptor for gRPC API. +func (in *GRPCAuthInterceptor) AuthenticationInterceptor(ctx context.Context, request any, serverInfo *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Errorf(codes.Unauthenticated, "failed to parse metadata from incoming context") + } + accessTokenStr, err := getTokenFromMetadata(md) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, err.Error()) + } + + userID, err := in.authenticate(ctx, accessTokenStr) + if err != nil { + if IsAuthenticationAllowed(serverInfo.FullMethod) { + return handler(ctx, request) + } + return nil, err + } + + // Stores userID into context. + childCtx := context.WithValue(ctx, UserIDContextKey, userID) + return handler(childCtx, request) +} + +func (in *GRPCAuthInterceptor) authenticate(ctx context.Context, accessTokenStr string) (int, error) { + if accessTokenStr == "" { + return 0, status.Errorf(codes.Unauthenticated, "access token not found") + } + claims := &claimsMessage{} + _, err := jwt.ParseWithClaims(accessTokenStr, claims, func(t *jwt.Token) (any, error) { + if t.Method.Alg() != jwt.SigningMethodHS256.Name { + return nil, status.Errorf(codes.Unauthenticated, "unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256) + } + if kid, ok := t.Header["kid"].(string); ok { + if kid == "v1" { + return []byte(in.secret), nil + } + } + return nil, status.Errorf(codes.Unauthenticated, "unexpected access token kid=%v", t.Header["kid"]) + }) + if err != nil { + return 0, status.Errorf(codes.Unauthenticated, "Invalid or expired access token") + } + if !audienceContains(claims.Audience, auth.AccessTokenAudienceName) { + return 0, status.Errorf(codes.Unauthenticated, + "invalid access token, audience mismatch, got %q, expected %q. you may send request to the wrong environment", + claims.Audience, + auth.AccessTokenAudienceName, + ) + } + + userID, err := strconv.Atoi(claims.Subject) + if err != nil { + return 0, status.Errorf(codes.Unauthenticated, "malformed ID %q in the access token", claims.Subject) + } + user, err := in.store.GetUser(ctx, &store.FindUser{ + ID: &userID, + }) + if err != nil { + return 0, status.Errorf(codes.Unauthenticated, "failed to find user ID %q in the access token", userID) + } + if user == nil { + return 0, status.Errorf(codes.Unauthenticated, "user ID %q not exists in the access token", userID) + } + if user.RowStatus == store.Archived { + return 0, status.Errorf(codes.Unauthenticated, "user ID %q has been deactivated by administrators", userID) + } + + return userID, nil +} + +func getTokenFromMetadata(md metadata.MD) (string, error) { + authorizationHeaders := md.Get("Authorization") + if len(md.Get("Authorization")) > 0 { + authHeaderParts := strings.Fields(authorizationHeaders[0]) + if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" { + return "", errors.Errorf("authorization header format must be Bearer {token}") + } + return authHeaderParts[1], nil + } + // check the HTTP cookie + var accessToken string + for _, t := range append(md.Get("grpcgateway-cookie"), md.Get("cookie")...) { + header := http.Header{} + header.Add("Cookie", t) + request := http.Request{Header: header} + if v, _ := request.Cookie(auth.AccessTokenCookieName); v != nil { + accessToken = v.Value + } + } + return accessToken, nil +} + +func audienceContains(audience jwt.ClaimStrings, token string) bool { + for _, v := range audience { + if v == token { + return true + } + } + return false +} + +type claimsMessage struct { + Name string `json:"name"` + jwt.RegisteredClaims +} + +// GenerateAccessToken generates an access token for web. +func GenerateAccessToken(username string, userID int, secret string) (string, error) { + expirationTime := time.Now().Add(auth.AccessTokenDuration) + return generateToken(username, userID, auth.AccessTokenAudienceName, expirationTime, []byte(secret)) +} + +func generateToken(username string, userID int, aud string, expirationTime time.Time, secret []byte) (string, error) { + // Create the JWT claims, which includes the username and expiry time. + claims := &claimsMessage{ + Name: username, + RegisteredClaims: jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{aud}, + // In JWT, the expiry time is expressed as unix milliseconds. + ExpiresAt: jwt.NewNumericDate(expirationTime), + IssuedAt: jwt.NewNumericDate(time.Now()), + Issuer: auth.Issuer, + Subject: strconv.Itoa(userID), + }, + } + + // Declare the token with the HS256 algorithm used for signing, and the claims. + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + token.Header["kid"] = auth.KeyID + + // Create the JWT string. + tokenString, err := token.SignedString(secret) + if err != nil { + return "", err + } + + return tokenString, nil +} diff --git a/api/v2/user_service.go b/api/v2/user_service.go new file mode 100644 index 0000000..661f016 --- /dev/null +++ b/api/v2/user_service.go @@ -0,0 +1,65 @@ +package v2 + +import ( + "context" + + apiv2pb "github.com/boojack/slash/proto/gen/api/v2" + "github.com/boojack/slash/store" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type UserService struct { + apiv2pb.UnimplementedUserServiceServer + + Store *store.Store +} + +// NewUserService creates a new UserService. +func NewUserService(store *store.Store) *UserService { + return &UserService{ + Store: store, + } +} + +func (s *UserService) GetUser(ctx context.Context, request *apiv2pb.GetUserRequest) (*apiv2pb.GetUserResponse, error) { + id := int(request.Id) + user, err := s.Store.GetUser(ctx, &store.FindUser{ + ID: &id, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list tags: %v", err) + } + if user == nil { + return nil, status.Errorf(codes.NotFound, "user not found") + } + + userMessage := convertUserFromStore(user) + response := &apiv2pb.GetUserResponse{ + User: userMessage, + } + return response, nil +} + +func convertUserFromStore(user *store.User) *apiv2pb.User { + return &apiv2pb.User{ + Id: int32(user.ID), + RowStatus: convertRowStatusFromStore(user.RowStatus), + CreatedTs: user.CreatedTs, + UpdatedTs: user.UpdatedTs, + Role: convertUserRoleFromStore(user.Role), + Email: user.Email, + Nickname: user.Nickname, + } +} + +func convertUserRoleFromStore(role store.Role) apiv2pb.Role { + switch role { + case store.RoleAdmin: + return apiv2pb.Role_ADMIN + case store.RoleUser: + return apiv2pb.Role_USER + default: + return apiv2pb.Role_ROLE_UNSPECIFIED + } +} diff --git a/api/v2/v2.go b/api/v2/v2.go new file mode 100644 index 0000000..451fcf4 --- /dev/null +++ b/api/v2/v2.go @@ -0,0 +1,67 @@ +package v2 + +import ( + "context" + "fmt" + + apiv2pb "github.com/boojack/slash/proto/gen/api/v2" + "github.com/boojack/slash/server/profile" + "github.com/boojack/slash/store" + grpcRuntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/labstack/echo/v4" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type APIV2Service struct { + Secret string + Profile *profile.Profile + Store *store.Store + + grpcServer *grpc.Server + grpcServerPort int +} + +func NewAPIV2Service(secret string, profile *profile.Profile, store *store.Store, grpcServerPort int) *APIV2Service { + authProvider := NewGRPCAuthInterceptor(store, secret) + grpcServer := grpc.NewServer( + grpc.ChainUnaryInterceptor( + authProvider.AuthenticationInterceptor, + ), + ) + apiv2pb.RegisterUserServiceServer(grpcServer, NewUserService(store)) + + return &APIV2Service{ + Secret: secret, + Profile: profile, + Store: store, + grpcServer: grpcServer, + grpcServerPort: grpcServerPort, + } +} + +func (s *APIV2Service) GetGRPCServer() *grpc.Server { + return s.grpcServer +} + +// RegisterGateway registers the gRPC-Gateway with the given Echo instance. +func (s *APIV2Service) RegisterGateway(ctx context.Context, e *echo.Echo) error { + // Create a client connection to the gRPC Server we just started. + // This is where the gRPC-Gateway proxies the requests. + conn, err := grpc.DialContext( + ctx, + fmt.Sprintf(":%d", s.grpcServerPort), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return err + } + + gwMux := grpcRuntime.NewServeMux() + if err := apiv2pb.RegisterUserServiceHandler(context.Background(), gwMux, conn); err != nil { + return err + } + e.Any("/api/v2/*", echo.WrapHandler(gwMux)) + + return nil +} diff --git a/proto/api/v2/user_service.proto b/proto/api/v2/user_service.proto index dabfea7..0e00191 100644 --- a/proto/api/v2/user_service.proto +++ b/proto/api/v2/user_service.proto @@ -10,8 +10,8 @@ option go_package = "gen/api/v2"; service UserService { rpc GetUser(GetUserRequest) returns (GetUserResponse) { - option (google.api.http) = {get: "/api/v2/users/{email}"}; - option (google.api.method_signature) = "email"; + option (google.api.http) = {get: "/api/v2/users/{id}"}; + option (google.api.method_signature) = "id"; } } @@ -40,7 +40,7 @@ enum Role { } message GetUserRequest { - string email = 1; + int32 id = 1; } message GetUserResponse { diff --git a/proto/gen/api/v2/README.md b/proto/gen/api/v2/README.md index 0fba93d..92d650c 100644 --- a/proto/gen/api/v2/README.md +++ b/proto/gen/api/v2/README.md @@ -63,7 +63,7 @@ | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | -| email | [string](#string) | | | +| id | [int32](#int32) | | | diff --git a/proto/gen/api/v2/user_service.pb.go b/proto/gen/api/v2/user_service.pb.go index 27805ea..5b1c201 100644 --- a/proto/gen/api/v2/user_service.pb.go +++ b/proto/gen/api/v2/user_service.pb.go @@ -170,7 +170,7 @@ type GetUserRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` } func (x *GetUserRequest) Reset() { @@ -205,11 +205,11 @@ func (*GetUserRequest) Descriptor() ([]byte, []int) { return file_api_v2_user_service_proto_rawDescGZIP(), []int{1} } -func (x *GetUserRequest) GetEmail() string { +func (x *GetUserRequest) GetId() int32 { if x != nil { - return x.Email + return x.Id } - return "" + return 0 } type GetUserResponse struct { @@ -283,36 +283,35 @@ var file_api_v2_user_service_proto_rawDesc = []byte{ 0x32, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x20, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x39, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, - 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x75, 0x73, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, - 0x72, 0x2a, 0x31, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x4f, 0x4c, - 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, - 0x09, 0x0a, 0x05, 0x41, 0x44, 0x4d, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x55, 0x53, - 0x45, 0x52, 0x10, 0x02, 0x32, 0x7c, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x6d, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1c, - 0x2e, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, - 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x73, - 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x55, - 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x25, 0xda, 0x41, 0x05, - 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x65, 0x6d, 0x61, 0x69, - 0x6c, 0x7d, 0x42, 0xa7, 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x73, 0x6c, 0x61, 0x73, 0x68, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x42, 0x10, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2f, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6f, 0x6f, 0x6a, 0x61, 0x63, 0x6b, 0x2f, - 0x73, 0x6c, 0x61, 0x73, 0x68, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, - 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x32, 0xa2, 0x02, 0x03, 0x53, - 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x53, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x41, 0x70, 0x69, 0x2e, 0x56, - 0x32, 0xca, 0x02, 0x0c, 0x53, 0x6c, 0x61, 0x73, 0x68, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, - 0xe2, 0x02, 0x18, 0x53, 0x6c, 0x61, 0x73, 0x68, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0x5c, - 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x53, 0x6c, - 0x61, 0x73, 0x68, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, + 0x22, 0x39, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x12, 0x2e, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, + 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x2a, 0x31, 0x0a, 0x04, 0x52, + 0x6f, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, + 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x44, 0x4d, + 0x49, 0x4e, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x55, 0x53, 0x45, 0x52, 0x10, 0x02, 0x32, 0x76, + 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x67, 0x0a, + 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x73, 0x6c, 0x61, 0x73, 0x68, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0xda, 0x41, 0x02, 0x69, 0x64, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x14, 0x12, 0x12, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x75, 0x73, 0x65, 0x72, + 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x42, 0xa7, 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x73, + 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x42, 0x10, 0x55, 0x73, 0x65, + 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, + 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6f, 0x6f, 0x6a, + 0x61, 0x63, 0x6b, 0x2f, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x32, + 0xa2, 0x02, 0x03, 0x53, 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x53, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x41, + 0x70, 0x69, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x0c, 0x53, 0x6c, 0x61, 0x73, 0x68, 0x5c, 0x41, 0x70, + 0x69, 0x5c, 0x56, 0x32, 0xe2, 0x02, 0x18, 0x53, 0x6c, 0x61, 0x73, 0x68, 0x5c, 0x41, 0x70, 0x69, + 0x5c, 0x56, 0x32, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, + 0x02, 0x0e, 0x53, 0x6c, 0x61, 0x73, 0x68, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x32, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/gen/api/v2/user_service.pb.gw.go b/proto/gen/api/v2/user_service.pb.gw.go index f95ab5d..42e4765 100644 --- a/proto/gen/api/v2/user_service.pb.gw.go +++ b/proto/gen/api/v2/user_service.pb.gw.go @@ -42,14 +42,14 @@ func request_UserService_GetUser_0(ctx context.Context, marshaler runtime.Marsha _ = err ) - val, ok = pathParams["email"] + val, ok = pathParams["id"] if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "email") + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") } - protoReq.Email, err = runtime.String(val) + protoReq.Id, err = runtime.Int32(val) if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "email", err) + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) } msg, err := client.GetUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) @@ -68,14 +68,14 @@ func local_request_UserService_GetUser_0(ctx context.Context, marshaler runtime. _ = err ) - val, ok = pathParams["email"] + val, ok = pathParams["id"] if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "email") + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") } - protoReq.Email, err = runtime.String(val) + protoReq.Id, err = runtime.Int32(val) if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "email", err) + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) } msg, err := server.GetUser(ctx, &protoReq) @@ -97,7 +97,7 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) var err error var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/slash.api.v2.UserService/GetUser", runtime.WithHTTPPathPattern("/api/v2/users/{email}")) + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/slash.api.v2.UserService/GetUser", runtime.WithHTTPPathPattern("/api/v2/users/{id}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -161,7 +161,7 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) var err error var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/slash.api.v2.UserService/GetUser", runtime.WithHTTPPathPattern("/api/v2/users/{email}")) + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/slash.api.v2.UserService/GetUser", runtime.WithHTTPPathPattern("/api/v2/users/{id}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -181,7 +181,7 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux } var ( - pattern_UserService_GetUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v2", "users", "email"}, "")) + pattern_UserService_GetUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v2", "users", "id"}, "")) ) var ( diff --git a/server/server.go b/server/server.go index 5ebb267..5c46145 100644 --- a/server/server.go +++ b/server/server.go @@ -3,13 +3,14 @@ package server import ( "context" "fmt" + "net" "time" apiv1 "github.com/boojack/slash/api/v1" + apiv2 "github.com/boojack/slash/api/v2" "github.com/boojack/slash/server/profile" "github.com/boojack/slash/store" "github.com/google/uuid" - "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) @@ -19,6 +20,9 @@ type Server struct { Profile *profile.Profile Store *store.Store + + // API services. + apiV2Service *apiv2.APIV2Service } func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store) (*Server, error) { @@ -66,10 +70,27 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store apiV1Service := apiv1.NewAPIV1Service(profile, store) apiV1Service.Start(rootGroup, secret) + s.apiV2Service = apiv2.NewAPIV2Service(secret, profile, store, s.Profile.Port+1) + // Register gRPC gateway as api v2. + if err := s.apiV2Service.RegisterGateway(ctx, e); err != nil { + return nil, fmt.Errorf("failed to register gRPC gateway: %w", err) + } + return s, nil } func (s *Server) Start(_ context.Context) error { + // Start gRPC server. + listen, err := net.Listen("tcp", fmt.Sprintf(":%d", s.Profile.Port+1)) + if err != nil { + return err + } + go func() { + if err := s.apiV2Service.GetGRPCServer().Serve(listen); err != nil { + println("grpc server listen error") + } + }() + return s.e.Start(fmt.Sprintf(":%d", s.Profile.Port)) }