From da9718abdbf52a98c176d0e6af29f29ddb6fac69 Mon Sep 17 00:00:00 2001 From: Sourr_cream Date: Thu, 18 Apr 2024 20:26:50 +0300 Subject: [PATCH] make grpc auth service --- Makefile | 10 +- backend/go.mod | 9 +- backend/go.sum | 20 +- backend/internal/app/grpc/app.go | 70 ++++ backend/internal/config/config.go | 3 +- backend/internal/grpc/auth/server.go | 99 +++++ backend/internal/lib/jwt/jwt.go | 36 ++ .../internal/protos/gen/go/daee/daee.pb.go | 361 ++++++++++++++++++ .../protos/gen/go/daee/daee_grpc.pb.go | 141 +++++++ backend/internal/protos/proto/daee/daee.proto | 28 ++ backend/internal/services/auth/auth.go | 146 +++++++ .../internal/storage/postgres/agents.sql.go | 20 +- .../storage/postgres/expressions.sql.go | 35 +- .../storage/postgres/model_transformers.go | 19 + backend/internal/storage/postgres/models.go | 9 +- .../storage/postgres/operations.sql.go | 11 +- .../internal/storage/postgres/users.sql.go | 43 +++ backend/internal/storage/storage.go | 7 + backend/sql/queries/agents.sql | 20 +- backend/sql/queries/expressions.sql | 29 +- backend/sql/queries/operations.sql | 11 +- backend/sql/queries/users.sql | 11 + backend/sql/schema/0001_expressions.sql | 8 +- backend/sql/schema/0008_users.sql | 11 + local.env | 3 +- 25 files changed, 1120 insertions(+), 40 deletions(-) create mode 100644 backend/internal/app/grpc/app.go create mode 100644 backend/internal/grpc/auth/server.go create mode 100644 backend/internal/lib/jwt/jwt.go create mode 100644 backend/internal/protos/gen/go/daee/daee.pb.go create mode 100644 backend/internal/protos/gen/go/daee/daee_grpc.pb.go create mode 100644 backend/internal/protos/proto/daee/daee.proto create mode 100644 backend/internal/services/auth/auth.go create mode 100644 backend/internal/storage/postgres/users.sql.go create mode 100644 backend/sql/queries/users.sql create mode 100644 backend/sql/schema/0008_users.sql diff --git a/Makefile b/Makefile index a224ae2..9446773 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,10 @@ -pull-rabbitmq: - docker pull rabbitmq:3-management - run-rabbitmq: docker run -d --name my-rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management run-postgres: - docker run --name habr-pg-13.3 -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=daee -d postgres:13.3 \ No newline at end of file + docker run --name habr-pg-13.3 -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=daee -d postgres:13.3 + +generate: + cd ./backend/internal/protos && protoc -I proto proto/daee/daee.proto \ + --go_out=./gen/go --go_opt=paths=source_relative \ + --go-grpc_out=./gen/go --go-grpc_opt=paths=source_relative \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod index 4eaf9c6..7acfbce 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -13,15 +13,22 @@ require github.com/lib/pq v1.10.9 require ( github.com/fatih/color v1.16.0 github.com/go-chi/chi/v5 v5.0.12 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/streadway/amqp v1.1.0 + golang.org/x/crypto v0.22.0 + google.golang.org/grpc v1.63.2 + google.golang.org/protobuf v1.33.0 ) require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index db61830..7eb4583 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -8,6 +8,10 @@ github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -21,10 +25,22 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM= github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/backend/internal/app/grpc/app.go b/backend/internal/app/grpc/app.go new file mode 100644 index 0000000..2c8e5b5 --- /dev/null +++ b/backend/internal/app/grpc/app.go @@ -0,0 +1,70 @@ +package grpcapp + +import ( + "fmt" + "log/slog" + "net" + + authgrpc "github.com/Prrromanssss/DAEE-fullstack/internal/grpc/auth" + "google.golang.org/grpc" +) + +type App struct { + log *slog.Logger + gRPCServer *grpc.Server + port int +} + +// MustRun runs gRPC server and panics if any error occurs. +func (a *App) MustRun() { + if err := a.Run(); err != nil { + panic(err) + } +} + +// New creates new gRPC server app. +func New( + log *slog.Logger, + authService authgrpc.Auth, + port int, +) *App { + gRPCServer := grpc.NewServer() + + authgrpc.Register(gRPCServer, authService) + + return &App{ + log: log, + gRPCServer: gRPCServer, + port: port, + } +} + +func (a *App) Run() error { + const op = "grpcapp.Run" + + log := a.log.With( + slog.String("op", op), + slog.Int("port", a.port), + ) + + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", a.port)) + if err != nil { + return fmt.Errorf("%s: %w", op, err) + } + log.Info("gRPC server is running", slog.String("addr", lis.Addr().String())) + + if err := a.gRPCServer.Serve(lis); err != nil { + return fmt.Errorf("%s: %w", op, err) + } + return nil +} + +// Stop stops gRPC server. +func (a *App) Stop() { + const op = "grpcapp.Stop" + + a.log.With(slog.String("op", op)). + Info("stopping gRPC server", slog.Int("port", a.port)) + + a.gRPCServer.GracefulStop() +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 3f3b9a6..c10cc8a 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -38,8 +38,9 @@ type DatabaseInstance struct { func MustLoad() *Config { err := godotenv.Load("local.env") if err != nil { - log.Fatalf("Can't parse env file: %v", err) + log.Fatalf("can't parse env file: %v", err) } + configPath := os.Getenv("CONFIG_PATH") if configPath == "" { log.Fatal("CONFIG_PATH is not set") diff --git a/backend/internal/grpc/auth/server.go b/backend/internal/grpc/auth/server.go new file mode 100644 index 0000000..4d238aa --- /dev/null +++ b/backend/internal/grpc/auth/server.go @@ -0,0 +1,99 @@ +package auth + +import ( + "context" + "errors" + + daeev1 "github.com/Prrromanssss/DAEE-fullstack/internal/protos/gen/go/daee" + "github.com/Prrromanssss/DAEE-fullstack/internal/services/auth" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type Auth interface { + Login( + ctx context.Context, + email string, + password string, + ) (token string, err error) + RegisterNewUser( + ctx context.Context, + email string, + password string, + ) (userID int64, err error) +} + +type serverAPI struct { + daeev1.UnimplementedAuthServer + auth Auth +} + +func Register(gRPC *grpc.Server, auth Auth) { + daeev1.RegisterAuthServer(gRPC, &serverAPI{auth: auth}) +} + +func (s *serverAPI) Login( + ctx context.Context, + req *daeev1.LoginRequest, +) (*daeev1.LoginResponse, error) { + if err := validateLogin(req); err != nil { + return nil, err + } + + token, err := s.auth.Login(ctx, req.GetEmail(), req.GetPassword()) + if err != nil { + if errors.Is(err, auth.ErrInvalidCredentials) { + return nil, status.Error(codes.InvalidArgument, "invalid email or password") + } + return nil, status.Error(codes.Internal, "internal error") + } + + return &daeev1.LoginResponse{ + Token: token, + }, nil +} + +func (s *serverAPI) Register( + ctx context.Context, + req *daeev1.RegisterRequest, +) (*daeev1.RegisterResponse, error) { + if err := validateRegister(req); err != nil { + return nil, err + } + + userID, err := s.auth.RegisterNewUser(ctx, req.GetEmail(), req.GetPassword()) + if err != nil { + if errors.Is(err, auth.ErrUserExists) { + return nil, status.Error(codes.AlreadyExists, "user already exists") + } + return nil, status.Error(codes.Internal, "internal error") + } + + return &daeev1.RegisterResponse{ + UserId: userID, + }, nil +} + +func validateLogin(req *daeev1.LoginRequest) error { + if req.GetEmail() == "" { + return status.Error(codes.InvalidArgument, "email is required") + } + + if req.GetPassword() == "" { + return status.Error(codes.InvalidArgument, "password is required") + } + + return nil +} + +func validateRegister(req *daeev1.RegisterRequest) error { + if req.GetEmail() == "" { + return status.Error(codes.InvalidArgument, "email is required") + } + + if req.GetPassword() == "" { + return status.Error(codes.InvalidArgument, "password is required") + } + return nil +} diff --git a/backend/internal/lib/jwt/jwt.go b/backend/internal/lib/jwt/jwt.go new file mode 100644 index 0000000..3723269 --- /dev/null +++ b/backend/internal/lib/jwt/jwt.go @@ -0,0 +1,36 @@ +package jwt + +import ( + "log" + "os" + "time" + + "github.com/Prrromanssss/DAEE-fullstack/internal/storage/postgres" + "github.com/golang-jwt/jwt/v5" + "github.com/joho/godotenv" +) + +func NewToken(user postgres.User, duration time.Duration) (string, error) { + err := godotenv.Load("local.env") + if err != nil { + log.Fatalf("can't parse env file: %v", err) + } + + jwtSecret := os.Getenv("JWT_SECRET") + if jwtSecret == "" { + log.Fatal("JWT_SECRET is not set") + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "uid": user.UserID, + "email": user.Email, + "exp": time.Now().Add(duration).Unix(), + }) + + tokenString, err := token.SignedString([]byte(jwtSecret)) + if err != nil { + return "", err + } + + return tokenString, nil +} diff --git a/backend/internal/protos/gen/go/daee/daee.pb.go b/backend/internal/protos/gen/go/daee/daee.pb.go new file mode 100644 index 0000000..e79b9b8 --- /dev/null +++ b/backend/internal/protos/gen/go/daee/daee.pb.go @@ -0,0 +1,361 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v4.25.3 +// source: daee/daee.proto + +package daeev1 + +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) +) + +type RegisterRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` // Email of the user to register. + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` // Password of ther user to register. +} + +func (x *RegisterRequest) Reset() { + *x = RegisterRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_daee_daee_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RegisterRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterRequest) ProtoMessage() {} + +func (x *RegisterRequest) ProtoReflect() protoreflect.Message { + mi := &file_daee_daee_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 RegisterRequest.ProtoReflect.Descriptor instead. +func (*RegisterRequest) Descriptor() ([]byte, []int) { + return file_daee_daee_proto_rawDescGZIP(), []int{0} +} + +func (x *RegisterRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *RegisterRequest) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +type RegisterResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` // User ID of the registered user. +} + +func (x *RegisterResponse) Reset() { + *x = RegisterResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_daee_daee_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RegisterResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterResponse) ProtoMessage() {} + +func (x *RegisterResponse) ProtoReflect() protoreflect.Message { + mi := &file_daee_daee_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 RegisterResponse.ProtoReflect.Descriptor instead. +func (*RegisterResponse) Descriptor() ([]byte, []int) { + return file_daee_daee_proto_rawDescGZIP(), []int{1} +} + +func (x *RegisterResponse) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +type LoginRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"` // Email of the user to login. + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` // Password of ther user to login. +} + +func (x *LoginRequest) Reset() { + *x = LoginRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_daee_daee_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LoginRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoginRequest) ProtoMessage() {} + +func (x *LoginRequest) ProtoReflect() protoreflect.Message { + mi := &file_daee_daee_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 LoginRequest.ProtoReflect.Descriptor instead. +func (*LoginRequest) Descriptor() ([]byte, []int) { + return file_daee_daee_proto_rawDescGZIP(), []int{2} +} + +func (x *LoginRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *LoginRequest) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +type LoginResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` // ID token of the logged user. +} + +func (x *LoginResponse) Reset() { + *x = LoginResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_daee_daee_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LoginResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoginResponse) ProtoMessage() {} + +func (x *LoginResponse) ProtoReflect() protoreflect.Message { + mi := &file_daee_daee_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 LoginResponse.ProtoReflect.Descriptor instead. +func (*LoginResponse) Descriptor() ([]byte, []int) { + return file_daee_daee_proto_rawDescGZIP(), []int{3} +} + +func (x *LoginResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +var File_daee_daee_proto protoreflect.FileDescriptor + +var file_daee_daee_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x64, 0x61, 0x65, 0x65, 0x2f, 0x64, 0x61, 0x65, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x43, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x73, + 0x74, 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, + 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x2b, 0x0a, 0x10, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x40, 0x0a, 0x0c, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 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, 0x12, + 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x25, 0x0a, 0x0d, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x32, 0x73, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x39, 0x0a, 0x08, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x15, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x12, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x1d, 0x5a, 0x1b, 0x70, 0x72, 0x72, 0x72, 0x6f, + 0x6d, 0x61, 0x6e, 0x73, 0x73, 0x73, 0x73, 0x2e, 0x64, 0x61, 0x65, 0x65, 0x2e, 0x76, 0x31, 0x3b, + 0x64, 0x61, 0x65, 0x65, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_daee_daee_proto_rawDescOnce sync.Once + file_daee_daee_proto_rawDescData = file_daee_daee_proto_rawDesc +) + +func file_daee_daee_proto_rawDescGZIP() []byte { + file_daee_daee_proto_rawDescOnce.Do(func() { + file_daee_daee_proto_rawDescData = protoimpl.X.CompressGZIP(file_daee_daee_proto_rawDescData) + }) + return file_daee_daee_proto_rawDescData +} + +var file_daee_daee_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_daee_daee_proto_goTypes = []interface{}{ + (*RegisterRequest)(nil), // 0: auth.RegisterRequest + (*RegisterResponse)(nil), // 1: auth.RegisterResponse + (*LoginRequest)(nil), // 2: auth.LoginRequest + (*LoginResponse)(nil), // 3: auth.LoginResponse +} +var file_daee_daee_proto_depIdxs = []int32{ + 0, // 0: auth.Auth.Register:input_type -> auth.RegisterRequest + 2, // 1: auth.Auth.Login:input_type -> auth.LoginRequest + 1, // 2: auth.Auth.Register:output_type -> auth.RegisterResponse + 3, // 3: auth.Auth.Login:output_type -> auth.LoginResponse + 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_daee_daee_proto_init() } +func file_daee_daee_proto_init() { + if File_daee_daee_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_daee_daee_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RegisterRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daee_daee_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RegisterResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daee_daee_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LoginRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daee_daee_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LoginResponse); 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_daee_daee_proto_rawDesc, + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_daee_daee_proto_goTypes, + DependencyIndexes: file_daee_daee_proto_depIdxs, + MessageInfos: file_daee_daee_proto_msgTypes, + }.Build() + File_daee_daee_proto = out.File + file_daee_daee_proto_rawDesc = nil + file_daee_daee_proto_goTypes = nil + file_daee_daee_proto_depIdxs = nil +} diff --git a/backend/internal/protos/gen/go/daee/daee_grpc.pb.go b/backend/internal/protos/gen/go/daee/daee_grpc.pb.go new file mode 100644 index 0000000..3fa02c6 --- /dev/null +++ b/backend/internal/protos/gen/go/daee/daee_grpc.pb.go @@ -0,0 +1,141 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v4.25.3 +// source: daee/daee.proto + +package daeev1 + +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 + +// AuthClient is the client API for Auth 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 AuthClient interface { + Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) + Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) +} + +type authClient struct { + cc grpc.ClientConnInterface +} + +func NewAuthClient(cc grpc.ClientConnInterface) AuthClient { + return &authClient{cc} +} + +func (c *authClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) { + out := new(RegisterResponse) + err := c.cc.Invoke(ctx, "/auth.Auth/Register", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) { + out := new(LoginResponse) + err := c.cc.Invoke(ctx, "/auth.Auth/Login", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AuthServer is the server API for Auth service. +// All implementations must embed UnimplementedAuthServer +// for forward compatibility +type AuthServer interface { + Register(context.Context, *RegisterRequest) (*RegisterResponse, error) + Login(context.Context, *LoginRequest) (*LoginResponse, error) + mustEmbedUnimplementedAuthServer() +} + +// UnimplementedAuthServer must be embedded to have forward compatible implementations. +type UnimplementedAuthServer struct { +} + +func (UnimplementedAuthServer) Register(context.Context, *RegisterRequest) (*RegisterResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Register not implemented") +} +func (UnimplementedAuthServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") +} +func (UnimplementedAuthServer) mustEmbedUnimplementedAuthServer() {} + +// UnsafeAuthServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AuthServer will +// result in compilation errors. +type UnsafeAuthServer interface { + mustEmbedUnimplementedAuthServer() +} + +func RegisterAuthServer(s grpc.ServiceRegistrar, srv AuthServer) { + s.RegisterService(&Auth_ServiceDesc, srv) +} + +func _Auth_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RegisterRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServer).Register(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/auth.Auth/Register", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServer).Register(ctx, req.(*RegisterRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Auth_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LoginRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServer).Login(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/auth.Auth/Login", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServer).Login(ctx, req.(*LoginRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Auth_ServiceDesc is the grpc.ServiceDesc for Auth service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Auth_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "auth.Auth", + HandlerType: (*AuthServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Register", + Handler: _Auth_Register_Handler, + }, + { + MethodName: "Login", + Handler: _Auth_Login_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "daee/daee.proto", +} diff --git a/backend/internal/protos/proto/daee/daee.proto b/backend/internal/protos/proto/daee/daee.proto new file mode 100644 index 0000000..40111b3 --- /dev/null +++ b/backend/internal/protos/proto/daee/daee.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package auth; + +option go_package = "prrromanssss.daee.v1;daeev1"; + +service Auth { + rpc Register (RegisterRequest) returns (RegisterResponse); + rpc Login(LoginRequest) returns (LoginResponse); +} + +message RegisterRequest { + string email = 1; // Email of the user to register. + string password = 2; // Password of ther user to register. +} + +message RegisterResponse { + int64 user_id = 1; // User ID of the registered user. +} + +message LoginRequest { + string email = 1; // Email of the user to login. + string password = 2; // Password of ther user to login. +} + +message LoginResponse { + string token = 1; // ID token of the logged user. +} \ No newline at end of file diff --git a/backend/internal/services/auth/auth.go b/backend/internal/services/auth/auth.go new file mode 100644 index 0000000..d56de85 --- /dev/null +++ b/backend/internal/services/auth/auth.go @@ -0,0 +1,146 @@ +package auth + +import ( + "context" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/Prrromanssss/DAEE-fullstack/internal/lib/jwt" + "github.com/Prrromanssss/DAEE-fullstack/internal/lib/logger/sl" + "github.com/Prrromanssss/DAEE-fullstack/internal/storage" + "github.com/Prrromanssss/DAEE-fullstack/internal/storage/postgres" + "golang.org/x/crypto/bcrypt" +) + +type Auth struct { + log *slog.Logger + usrSaver UserSaver + usrProvider UserProvider + tokenTTL time.Duration +} + +type UserSaver interface { + SaveUser( + ctx context.Context, + email string, + passHash []byte, + ) (uid int64, err error) +} + +type UserProvider interface { + User(ctx context.Context, email string) (postgres.User, error) + IsAdmin(ctx context.Context, userID int64) (bool, error) +} + +var ( + ErrInvalidCredentials = errors.New("invalid credentials") + ErrInvalidAppID = errors.New("invalid app id") + ErrUserExists = errors.New("user already exists") + ErrUserNotFound = errors.New("user not found") +) + +// New returns a new instance of the Auth service. +func New( + log *slog.Logger, + userSaver UserSaver, + userProvider UserProvider, + tokenTTL time.Duration, +) *Auth { + return &Auth{ + log: log, + usrSaver: userSaver, + usrProvider: userProvider, + tokenTTL: tokenTTL, + } +} + +// Login checks if user with given credentials exists in the system and returns access token. +// +// If user exists, but password is incorrect, returns error. +// If user doesn't exist, returns error. +func (a *Auth) Login( + ctx context.Context, + email string, + password string, +) (string, error) { + const op = "auth.Login" + + log := a.log.With( + slog.String("op", op), + slog.String("username", email), + ) + + log.Info("attempting to login user") + + user, err := a.usrProvider.User(ctx, email) + if err != nil { + if errors.Is(err, storage.ErrUserNotFound) { + a.log.Warn("user not found", sl.Err(err)) + + return "", fmt.Errorf("%s: %w", op, ErrInvalidCredentials) + } + + a.log.Error("failed to get user", sl.Err(err)) + + return "", fmt.Errorf("%s: %w", op, err) + } + + if err := bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(password)); err != nil { + a.log.Info("invalid credentials", sl.Err(err)) + + return "", fmt.Errorf("%s: %w", op, ErrInvalidCredentials) + } + + log.Info("user logged successfully") + + token, err := jwt.NewToken(user, a.tokenTTL) + if err != nil { + a.log.Error("failed to generate token", sl.Err(err)) + + return "", fmt.Errorf("%s: %w", op, err) + } + + return token, nil +} + +// RegisterNewUser registers new user in the system and returns user ID. +// If user with given username already exists, returns error. +func (a *Auth) RegisterNewUser( + ctx context.Context, + email string, + pass string, +) (int64, error) { + const op = "auth.RegisterNewUser" + + log := a.log.With( + slog.String("op", op), + slog.String("email", email), + ) + + log.Info("registering user") + + passHash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) + if err != nil { + log.Error("failed to generate password hash", sl.Err(err)) + + return 0, fmt.Errorf("%s: %w", op, err) + } + + id, err := a.usrSaver.SaveUser(ctx, email, passHash) + if err != nil { + if errors.Is(err, storage.ErrUserExists) { + log.Warn("user already exists", sl.Err(err)) + + return 0, fmt.Errorf("%s: %w", op, ErrUserExists) + } + log.Error("failed to save user", sl.Err(err)) + + return 0, fmt.Errorf("%s: %w", op, err) + } + + log.Info("user registered") + + return id, nil +} diff --git a/backend/internal/storage/postgres/agents.sql.go b/backend/internal/storage/postgres/agents.sql.go index e8daa6f..a1fc477 100644 --- a/backend/internal/storage/postgres/agents.sql.go +++ b/backend/internal/storage/postgres/agents.sql.go @@ -11,9 +11,14 @@ import ( ) const createAgent = `-- name: CreateAgent :one -INSERT INTO agents (created_at, number_of_parallel_calculations, last_ping, status) -VALUES ($1, $2, $3, $4) -RETURNING agent_id, number_of_parallel_calculations, last_ping, status, created_at, number_of_active_calculations +INSERT INTO agents + (created_at, number_of_parallel_calculations, last_ping, status) +VALUES + ($1, $2, $3, $4) +RETURNING + agent_id, number_of_parallel_calculations, + last_ping, status, created_at, + number_of_active_calculations ` type CreateAgentParams struct { @@ -63,7 +68,8 @@ func (q *Queries) DeleteAgents(ctx context.Context) error { } const getAgentByID = `-- name: GetAgentByID :one -SELECT agent_id, number_of_parallel_calculations, last_ping, status, created_at, number_of_active_calculations FROM agents +SELECT agent_id, number_of_parallel_calculations, last_ping, status, created_at, number_of_active_calculations +FROM agents WHERE agent_id = $1 ` @@ -82,7 +88,11 @@ func (q *Queries) GetAgentByID(ctx context.Context, agentID int32) (Agent, error } const getAgents = `-- name: GetAgents :many -SELECT agent_id, number_of_parallel_calculations, last_ping, status, created_at, number_of_active_calculations FROM agents +SELECT + agent_id, number_of_parallel_calculations, + last_ping, status, created_at, + number_of_active_calculations +FROM agents ORDER BY created_at DESC ` diff --git a/backend/internal/storage/postgres/expressions.sql.go b/backend/internal/storage/postgres/expressions.sql.go index f20e934..4738821 100644 --- a/backend/internal/storage/postgres/expressions.sql.go +++ b/backend/internal/storage/postgres/expressions.sql.go @@ -12,9 +12,14 @@ import ( ) const createExpression = `-- name: CreateExpression :one -INSERT INTO expressions (created_at, updated_at, data, parse_data, status) -VALUES ($1, $2, $3, $4, $5) -RETURNING expression_id, agent_id, created_at, updated_at, data, parse_data, status, result, is_ready +INSERT INTO expressions + (created_at, updated_at, data, parse_data, status, user_id) +VALUES + ($1, $2, $3, $4, $5, $6) +RETURNING + expression_id, user_id, agent_id, + created_at, updated_at, data, parse_data, + status, result, is_ready ` type CreateExpressionParams struct { @@ -23,6 +28,7 @@ type CreateExpressionParams struct { Data string ParseData string Status ExpressionStatus + UserID int32 } func (q *Queries) CreateExpression(ctx context.Context, arg CreateExpressionParams) (Expression, error) { @@ -32,10 +38,12 @@ func (q *Queries) CreateExpression(ctx context.Context, arg CreateExpressionPara arg.Data, arg.ParseData, arg.Status, + arg.UserID, ) var i Expression err := row.Scan( &i.ExpressionID, + &i.UserID, &i.AgentID, &i.CreatedAt, &i.UpdatedAt, @@ -49,7 +57,11 @@ func (q *Queries) CreateExpression(ctx context.Context, arg CreateExpressionPara } const getComputingExpressions = `-- name: GetComputingExpressions :many -SELECT expression_id, agent_id, created_at, updated_at, data, parse_data, status, result, is_ready FROM expressions +SELECT + expression_id, user_id, agent_id, + created_at, updated_at, data, parse_data, + status, result, is_ready +FROM expressions WHERE status = 'computing' ORDER BY created_at DESC ` @@ -65,6 +77,7 @@ func (q *Queries) GetComputingExpressions(ctx context.Context) ([]Expression, er var i Expression if err := rows.Scan( &i.ExpressionID, + &i.UserID, &i.AgentID, &i.CreatedAt, &i.UpdatedAt, @@ -88,7 +101,11 @@ func (q *Queries) GetComputingExpressions(ctx context.Context) ([]Expression, er } const getExpressionByID = `-- name: GetExpressionByID :one -SELECT expression_id, agent_id, created_at, updated_at, data, parse_data, status, result, is_ready FROM expressions +SELECT + expression_id, user_id, agent_id, + created_at, updated_at, data, parse_data, + status, result, is_ready +FROM expressions WHERE expression_id = $1 ` @@ -97,6 +114,7 @@ func (q *Queries) GetExpressionByID(ctx context.Context, expressionID int32) (Ex var i Expression err := row.Scan( &i.ExpressionID, + &i.UserID, &i.AgentID, &i.CreatedAt, &i.UpdatedAt, @@ -110,7 +128,11 @@ func (q *Queries) GetExpressionByID(ctx context.Context, expressionID int32) (Ex } const getExpressions = `-- name: GetExpressions :many -SELECT expression_id, agent_id, created_at, updated_at, data, parse_data, status, result, is_ready FROM expressions +SELECT + expression_id, user_id, agent_id, + created_at, updated_at, data, parse_data, + status, result, is_ready +FROM expressions ORDER BY created_at DESC ` @@ -125,6 +147,7 @@ func (q *Queries) GetExpressions(ctx context.Context) ([]Expression, error) { var i Expression if err := rows.Scan( &i.ExpressionID, + &i.UserID, &i.AgentID, &i.CreatedAt, &i.UpdatedAt, diff --git a/backend/internal/storage/postgres/model_transformers.go b/backend/internal/storage/postgres/model_transformers.go index e719c9f..3b4be58 100644 --- a/backend/internal/storage/postgres/model_transformers.go +++ b/backend/internal/storage/postgres/model_transformers.go @@ -7,6 +7,7 @@ import ( type ExpressionTransformed struct { ExpressionID int32 `json:"expression_id"` + UserID int32 `json:"user_id"` AgentID sql.NullInt32 `json:"agent_id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -67,3 +68,21 @@ func DatabaseAgentsToAgents(dbAgents []Agent) []AgentTransformed { } return agents } + +type UserTransformed struct { + UserID int32 `json:"user_id"` + Email string `json:"email"` + PasswordHash []byte `json:"password_hash"` +} + +func DatabaseUserToUser(dbUser User) UserTransformed { + return UserTransformed(dbUser) +} + +func DatabaseUsersToUsers(dbUsers []User) []UserTransformed { + users := []UserTransformed{} + for _, dbUser := range dbUsers { + users = append(users, DatabaseUserToUser(dbUser)) + } + return users +} diff --git a/backend/internal/storage/postgres/models.go b/backend/internal/storage/postgres/models.go index bef13ec..738089b 100644 --- a/backend/internal/storage/postgres/models.go +++ b/backend/internal/storage/postgres/models.go @@ -58,7 +58,7 @@ func (ns NullAgentStatus) Value() (driver.Value, error) { type ExpressionStatus string const ( - ExpressionStatusReadyforcomputation ExpressionStatus = "ready for computation" + ExpressionStatusReadyForComputation ExpressionStatus = "ready_for_computation" ExpressionStatusComputing ExpressionStatus = "computing" ExpressionStatusResult ExpressionStatus = "result" ExpressionStatusTerminated ExpressionStatus = "terminated" @@ -110,6 +110,7 @@ type Agent struct { type Expression struct { ExpressionID int32 + UserID int32 AgentID sql.NullInt32 CreatedAt time.Time UpdatedAt time.Time @@ -125,3 +126,9 @@ type Operation struct { OperationType string ExecutionTime int32 } + +type User struct { + UserID int32 + Email string + PasswordHash []byte +} diff --git a/backend/internal/storage/postgres/operations.sql.go b/backend/internal/storage/postgres/operations.sql.go index 15255ec..54e330c 100644 --- a/backend/internal/storage/postgres/operations.sql.go +++ b/backend/internal/storage/postgres/operations.sql.go @@ -12,7 +12,8 @@ import ( const createOperation = `-- name: CreateOperation :exec INSERT INTO operations (operation_type, execution_time) VALUES ($1, $2) -RETURNING operation_id, operation_type, execution_time +RETURNING + operation_id, operation_type, execution_time ` type CreateOperationParams struct { @@ -26,7 +27,9 @@ func (q *Queries) CreateOperation(ctx context.Context, arg CreateOperationParams } const getOperationByType = `-- name: GetOperationByType :one -SELECT operation_id, operation_type, execution_time FROM operations +SELECT + operation_id, operation_type, execution_time +FROM operations WHERE operation_type = $1 ` @@ -50,7 +53,9 @@ func (q *Queries) GetOperationTimeByType(ctx context.Context, operationType stri } const getOperations = `-- name: GetOperations :many -SELECT operation_id, operation_type, execution_time FROM operations +SELECT + operation_id, operation_type, execution_time +FROM operations ORDER BY operation_type DESC ` diff --git a/backend/internal/storage/postgres/users.sql.go b/backend/internal/storage/postgres/users.sql.go new file mode 100644 index 0000000..44b7f49 --- /dev/null +++ b/backend/internal/storage/postgres/users.sql.go @@ -0,0 +1,43 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 +// source: users.sql + +package postgres + +import ( + "context" +) + +const getUser = `-- name: GetUser :one +SELECT user_id, email, password_hash +FROM users +WHERE user_id = $1 +` + +func (q *Queries) GetUser(ctx context.Context, userID int32) (User, error) { + row := q.db.QueryRowContext(ctx, getUser, userID) + var i User + err := row.Scan(&i.UserID, &i.Email, &i.PasswordHash) + return i, err +} + +const saveUser = `-- name: SaveUser :one +INSERT INTO users + (email, password_hash) +VALUES + ($1, $2) +RETURNING user_id +` + +type SaveUserParams struct { + Email string + PasswordHash []byte +} + +func (q *Queries) SaveUser(ctx context.Context, arg SaveUserParams) (int32, error) { + row := q.db.QueryRowContext(ctx, saveUser, arg.Email, arg.PasswordHash) + var user_id int32 + err := row.Scan(&user_id) + return user_id, err +} diff --git a/backend/internal/storage/storage.go b/backend/internal/storage/storage.go index 28f6a95..af5caae 100644 --- a/backend/internal/storage/storage.go +++ b/backend/internal/storage/storage.go @@ -2,12 +2,19 @@ package storage import ( "database/sql" + "errors" "log" "github.com/Prrromanssss/DAEE-fullstack/internal/storage/postgres" _ "github.com/lib/pq" ) +var ( + ErrUserExists = errors.New("user already exists") + ErrUserNotFound = errors.New("user not found") + ErrAppNotFound = errors.New("app not found") +) + type Storage struct { DB *postgres.Queries } diff --git a/backend/sql/queries/agents.sql b/backend/sql/queries/agents.sql index 826a109..2dd89db 100644 --- a/backend/sql/queries/agents.sql +++ b/backend/sql/queries/agents.sql @@ -1,14 +1,24 @@ -- name: CreateAgent :one -INSERT INTO agents (created_at, number_of_parallel_calculations, last_ping, status) -VALUES ($1, $2, $3, $4) -RETURNING *; +INSERT INTO agents + (created_at, number_of_parallel_calculations, last_ping, status) +VALUES + ($1, $2, $3, $4) +RETURNING + agent_id, number_of_parallel_calculations, + last_ping, status, created_at, + number_of_active_calculations; -- name: GetAgents :many -SELECT * FROM agents +SELECT + agent_id, number_of_parallel_calculations, + last_ping, status, created_at, + number_of_active_calculations +FROM agents ORDER BY created_at DESC; -- name: GetAgentByID :one -SELECT * FROM agents +SELECT * +FROM agents WHERE agent_id = $1; -- name: UpdateAgentLastPing :exec diff --git a/backend/sql/queries/expressions.sql b/backend/sql/queries/expressions.sql index a7d78b2..d9e08e2 100644 --- a/backend/sql/queries/expressions.sql +++ b/backend/sql/queries/expressions.sql @@ -1,14 +1,27 @@ -- name: CreateExpression :one -INSERT INTO expressions (created_at, updated_at, data, parse_data, status) -VALUES ($1, $2, $3, $4, $5) -RETURNING *; +INSERT INTO expressions + (created_at, updated_at, data, parse_data, status, user_id) +VALUES + ($1, $2, $3, $4, $5, $6) +RETURNING + expression_id, user_id, agent_id, + created_at, updated_at, data, parse_data, + status, result, is_ready; -- name: GetExpressions :many -SELECT * FROM expressions +SELECT + expression_id, user_id, agent_id, + created_at, updated_at, data, parse_data, + status, result, is_ready +FROM expressions ORDER BY created_at DESC; -- name: GetExpressionByID :one -SELECT * FROM expressions +SELECT + expression_id, user_id, agent_id, + created_at, updated_at, data, parse_data, + status, result, is_ready +FROM expressions WHERE expression_id = $1; -- name: UpdateExpressionData :exec @@ -32,7 +45,11 @@ SET status = $1 WHERE expression_id = $2; -- name: GetComputingExpressions :many -SELECT * FROM expressions +SELECT + expression_id, user_id, agent_id, + created_at, updated_at, data, parse_data, + status, result, is_ready +FROM expressions WHERE status = 'computing' ORDER BY created_at DESC; diff --git a/backend/sql/queries/operations.sql b/backend/sql/queries/operations.sql index e67aba3..7c63bf8 100644 --- a/backend/sql/queries/operations.sql +++ b/backend/sql/queries/operations.sql @@ -1,7 +1,8 @@ -- name: CreateOperation :exec INSERT INTO operations (operation_type, execution_time) VALUES ($1, $2) -RETURNING *; +RETURNING + operation_id, operation_type, execution_time; -- name: UpdateOperationTime :one UPDATE operations @@ -10,7 +11,9 @@ WHERE operation_type = $2 RETURNING *; -- name: GetOperations :many -SELECT * FROM operations +SELECT + operation_id, operation_type, execution_time +FROM operations ORDER BY operation_type DESC; -- name: GetOperationTimeByType :one @@ -18,5 +21,7 @@ SELECT execution_time FROM operations WHERE operation_type = $1; -- name: GetOperationByType :one -SELECT * FROM operations +SELECT + operation_id, operation_type, execution_time +FROM operations WHERE operation_type = $1; diff --git a/backend/sql/queries/users.sql b/backend/sql/queries/users.sql new file mode 100644 index 0000000..be7d123 --- /dev/null +++ b/backend/sql/queries/users.sql @@ -0,0 +1,11 @@ +-- name: GetUser :one +SELECT user_id, email, password_hash +FROM users +WHERE user_id = $1; + +-- name: SaveUser :one +INSERT INTO users + (email, password_hash) +VALUES + ($1, $2) +RETURNING user_id; \ No newline at end of file diff --git a/backend/sql/schema/0001_expressions.sql b/backend/sql/schema/0001_expressions.sql index 785204a..f58c116 100644 --- a/backend/sql/schema/0001_expressions.sql +++ b/backend/sql/schema/0001_expressions.sql @@ -1,9 +1,10 @@ -- +goose Up DROP TYPE IF EXISTS expression_status; -CREATE TYPE expression_status AS ENUM ('ready for computation', 'computing', 'result', 'terminated'); +CREATE TYPE expression_status AS ENUM ('ready_for_computation', 'computing', 'result', 'terminated'); CREATE TABLE IF NOT EXISTS expressions ( expression_id int GENERATED ALWAYS AS IDENTITY, + user_id int NOT NULL, agent_id int, created_at timestamp NOT NULL, updated_at timestamp NOT NULL, @@ -16,7 +17,10 @@ CREATE TABLE IF NOT EXISTS expressions ( PRIMARY KEY(expression_id), FOREIGN KEY(agent_id) REFERENCES agents(agent_id) - ON DELETE SET NULL + ON DELETE SET NULL, + FOREIGN KEY(user_id) + REFERENCES users(user_id) + ON DELETE CASCADE ); diff --git a/backend/sql/schema/0008_users.sql b/backend/sql/schema/0008_users.sql new file mode 100644 index 0000000..e127748 --- /dev/null +++ b/backend/sql/schema/0008_users.sql @@ -0,0 +1,11 @@ +-- +goose Up +CREATE TABLE IF NOT EXISTS users ( + user_id int GENERATED ALWAYS AS IDENTITY, + email text UNIQUE NOT NULL, + password_hash bytea NOT NULL, + + PRIMARY KEY(user_id) +); + +-- +goose Down +DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/local.env b/local.env index 1a0da05..5879c6b 100644 --- a/local.env +++ b/local.env @@ -1 +1,2 @@ -CONFIG_PATH="./backend/config/local.yaml" \ No newline at end of file +CONFIG_PATH="./backend/config/local.yaml" +JWT_SECRET="slim_shady_slim_shady" \ No newline at end of file