diff --git a/Makefile b/Makefile index d6750e9..ec103d8 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,6 @@ testci: server: go run main.go - protoc: rm -f pb/*.go protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative \ diff --git a/gapi/converter.go b/gapi/converter.go new file mode 100644 index 0000000..a7d00c5 --- /dev/null +++ b/gapi/converter.go @@ -0,0 +1,17 @@ +package gapi + +import ( + db "github.com/aradwann/eenergy/db/store" + "github.com/aradwann/eenergy/pb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func convertUser(user db.User) *pb.User { + return &pb.User{ + Username: user.Username, + FullName: user.FullName, + Email: user.Email, + PasswordChangedAt: timestamppb.New(user.PasswordChangedAt), + CreatedAt: timestamppb.New(user.CreatedAt), + } +} diff --git a/gapi/rpc_create_user.go b/gapi/rpc_create_user.go new file mode 100644 index 0000000..9b8e8cc --- /dev/null +++ b/gapi/rpc_create_user.go @@ -0,0 +1,38 @@ +package gapi + +import ( + "context" + + db "github.com/aradwann/eenergy/db/store" + "github.com/aradwann/eenergy/pb" + "github.com/aradwann/eenergy/util" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (server *Server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) { + hashedPassword, err := util.HashPassword(req.GetPassword()) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to hash password: %s", err) + } + + arg := db.CreateUserParams{ + Username: req.GetUsername(), + HashedPassword: hashedPassword, + FullName: req.GetFullName(), + Email: req.GetEmail(), + } + + user, err := server.store.CreateUser(ctx, arg) + if err != nil { + if db.ErrorCode(err) == db.UniqueViolation { + return nil, status.Errorf(codes.AlreadyExists, "username already exsits") + } + return nil, status.Errorf(codes.Internal, "failed to create user: %s", err) + } + + rsp := &pb.CreateUserResponse{ + User: convertUser(user), + } + return rsp, nil +} diff --git a/gapi/rpc_login_user.go b/gapi/rpc_login_user.go new file mode 100644 index 0000000..b13e4c4 --- /dev/null +++ b/gapi/rpc_login_user.go @@ -0,0 +1,70 @@ +package gapi + +import ( + "context" + "errors" + + db "github.com/aradwann/eenergy/db/store" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/aradwann/eenergy/pb" + "github.com/aradwann/eenergy/util" +) + +func (server *Server) LoginUser(ctx context.Context, req *pb.LoginUserRequest) (*pb.LoginUserResponse, error) { + user, err := server.store.GetUser(ctx, req.GetUsername()) + if err != nil { + if errors.Is(err, db.ErrRecordNotFound) { + return nil, status.Errorf(codes.NotFound, "user not found") + + } + return nil, status.Errorf(codes.Internal, "failed to find user") + } + + err = util.CheckPassword(req.GetPassword(), user.HashedPassword) + if err != nil { + return nil, status.Errorf(codes.NotFound, "incorrect password") + + } + + accessToken, accessPayload, err := server.tokenMaker.CreateToken( + user.Username, + server.config.AccessTokenDuration, + ) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create access token") + } + + refreshToken, refreshPayload, err := server.tokenMaker.CreateToken( + user.Username, + server.config.RefreshTokenDuration, + ) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create refresh token") + } + + session, err := server.store.CreateSession(ctx, db.CreateSessionParams{ + ID: refreshPayload.ID, + Username: user.Username, + RefreshToken: refreshToken, + UserAgent: "ctx.Request.UserAgent()", + ClientIp: "ctx.ClientIP()", + IsBlocked: false, + ExpiresAt: refreshPayload.ExpiredAt, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create session") + } + + rsp := &pb.LoginUserResponse{ + User: convertUser(user), + SessionId: session.ID.String(), + AccessToken: accessToken, + AccessTokenExpiresAt: timestamppb.New(accessPayload.ExpiredAt), + RefreshToken: refreshToken, + RefreshTokenExpiresAt: timestamppb.New(refreshPayload.ExpiredAt), + } + return rsp, nil +} diff --git a/gapi/server.go b/gapi/server.go new file mode 100644 index 0000000..3042360 --- /dev/null +++ b/gapi/server.go @@ -0,0 +1,34 @@ +package gapi + +import ( + "fmt" + + db "github.com/aradwann/eenergy/db/store" + "github.com/aradwann/eenergy/pb" + "github.com/aradwann/eenergy/token" + "github.com/aradwann/eenergy/util" +) + +// Server serves gRPC requests for our banking service. +type Server struct { + pb.UnimplementedEenergyServiceServer + config util.Config + store db.Store + tokenMaker token.Maker +} + +// NewServer creates a new gRPC server. +func NewServer(config util.Config, store db.Store) (*Server, error) { + tokenMaker, err := token.NewPASETOMaker(config.TokenSymmetricKey) + if err != nil { + return nil, fmt.Errorf("cannot create token maker: %w", err) + } + + server := &Server{ + config: config, + store: store, + tokenMaker: tokenMaker, + } + + return server, nil +} diff --git a/main.go b/main.go index 7738da0..92c43d7 100644 --- a/main.go +++ b/main.go @@ -3,13 +3,18 @@ package main import ( "database/sql" "fmt" + "log" + "net" "os" - "github.com/aradwann/eenergy/api" db "github.com/aradwann/eenergy/db/store" + "github.com/aradwann/eenergy/gapi" + "github.com/aradwann/eenergy/pb" "github.com/aradwann/eenergy/util" _ "github.com/golang-migrate/migrate/v4/source/file" _ "github.com/jackc/pgx/v5/stdlib" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" ) func main() { @@ -34,19 +39,42 @@ func main() { store := db.NewStore(dbConn) - runGinServer(config, store) + runGrpcServer(config, store) } -func runGinServer(config util.Config, store db.Store) { - server, err := api.NewServer(config, store) +// func runGinServer(config util.Config, store db.Store) { +// server, err := api.NewServer(config, store) +// if err != nil { +// // log.Fatal().Err(err).Msg("cannot create server") +// fmt.Fprintf(os.Stderr, "cannot create server: %v\n", err) +// } + +// err = server.Start(config.HTTPServerAddress) +// if err != nil { +// // log.Fatal().Err(err).Msg("cannot start server") +// fmt.Fprintf(os.Stderr, "cannot start server: %v\n", err) +// } +// } + +func runGrpcServer(config util.Config, store db.Store) { + server, err := gapi.NewServer(config, store) + if err != nil { + log.Fatal("cannot create server") + } + + // gprcLogger := grpc.UnaryInterceptor(gapi.GrpcLogger) + grpcServer := grpc.NewServer() + pb.RegisterEenergyServiceServer(grpcServer, server) + reflection.Register(grpcServer) + + listener, err := net.Listen("tcp", config.GRPCServerAddress) if err != nil { - // log.Fatal().Err(err).Msg("cannot create server") - fmt.Fprintf(os.Stderr, "cannot create server: %v\n", err) + log.Fatal("cannot create listener") } - err = server.Start(config.HTTPServerAddress) + log.Printf("start gRPC server at %s", listener.Addr().String()) + err = grpcServer.Serve(listener) if err != nil { - // log.Fatal().Err(err).Msg("cannot start server") - fmt.Fprintf(os.Stderr, "cannot start server: %v\n", err) + log.Fatal("cannot start gRPC server") } } diff --git a/util/config.go b/util/config.go index 2f9a1e6..954792f 100644 --- a/util/config.go +++ b/util/config.go @@ -14,6 +14,7 @@ type Config struct { DBSource string `mapstructure:"DB_SOURCE"` MigrationsURL string `mapstructure:"MIGRATIONS_URL"` HTTPServerAddress string `mapstructure:"HTTP_SERVER_ADDRESS"` + GRPCServerAddress string `mapstructure:"GRPC_SERVER_ADDRESS"` RedisAddress string `mapstructure:"REDIS_ADDRESS"` TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"` AccessTokenDuration time.Duration `mapstructure:"ACCESS_TOKEN_DURATION"`