// Package main provides FX providers for Identity Service. // This file creates the service inline to avoid importing internal packages. package main import ( "context" "crypto/rand" "crypto/subtle" "encoding/base64" "fmt" "math" "net" "strings" "time" identityv1 "git.dcentral.systems/toolz/goplt/api/proto/generated/identity/v1" "git.dcentral.systems/toolz/goplt/internal/ent" "git.dcentral.systems/toolz/goplt/internal/ent/user" "git.dcentral.systems/toolz/goplt/internal/infra/database" "git.dcentral.systems/toolz/goplt/pkg/config" "git.dcentral.systems/toolz/goplt/pkg/logger" "go.uber.org/fx" "go.uber.org/zap" "golang.org/x/crypto/argon2" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/health" "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/reflection" "google.golang.org/grpc/status" ) // userService provides user management functionality. type userService struct { client *database.Client logger logger.Logger } // generateToken generates a random token. func generateToken() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", fmt.Errorf("failed to generate token: %w", err) } return fmt.Sprintf("%x", b), nil } // hashPassword hashes a password using argon2id. func hashPassword(password string) (string, error) { salt := make([]byte, 16) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("failed to generate salt: %w", err) } hash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, 32) b64Salt := base64.RawStdEncoding.EncodeToString(salt) b64Hash := base64.RawStdEncoding.EncodeToString(hash) return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, 64*1024, 3, 4, b64Salt, b64Hash), nil } // verifyPassword verifies a password against a hash. func verifyPassword(password, hash string) (bool, error) { // Simplified verification - in production use proper parsing parts := strings.Split(hash, "$") if len(parts) != 6 { return false, fmt.Errorf("invalid hash format") } salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return false, err } expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil { return false, err } hashLen := len(expectedHash) if hashLen < 0 || hashLen > math.MaxUint32 { return false, fmt.Errorf("invalid hash length: %d", hashLen) } actualHash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, uint32(hashLen)) return subtle.ConstantTimeCompare(expectedHash, actualHash) == 1, nil } // createUser creates a new user. func (s *userService) createUser(ctx context.Context, email, username, pwd, firstName, lastName string) (*ent.User, error) { exists, err := s.client.User.Query().Where(user.Email(email)).Exist(ctx) if err != nil { return nil, fmt.Errorf("failed to check email: %w", err) } if exists { return nil, fmt.Errorf("email already exists") } passwordHash, err := hashPassword(pwd) if err != nil { return nil, fmt.Errorf("failed to hash password: %w", err) } verificationToken, err := generateToken() if err != nil { return nil, fmt.Errorf("failed to generate token: %w", err) } create := s.client.User.Create(). SetID(fmt.Sprintf("%d", time.Now().UnixNano())). SetEmail(email). SetPasswordHash(passwordHash). SetVerified(false). SetEmailVerificationToken(verificationToken) if username != "" { create = create.SetUsername(username) } if firstName != "" { create = create.SetFirstName(firstName) } if lastName != "" { create = create.SetLastName(lastName) } u, err := create.Save(ctx) if err != nil { return nil, fmt.Errorf("failed to create user: %w", err) } return u, nil } // getUser retrieves a user by ID. func (s *userService) getUser(ctx context.Context, id string) (*ent.User, error) { u, err := s.client.User.Get(ctx, id) if err != nil { return nil, fmt.Errorf("failed to get user: %w", err) } return u, nil } // getUserByEmail retrieves a user by email. func (s *userService) getUserByEmail(ctx context.Context, email string) (*ent.User, error) { u, err := s.client.User.Query().Where(user.Email(email)).Only(ctx) if err != nil { return nil, fmt.Errorf("failed to get user: %w", err) } return u, nil } // updateUser updates a user. func (s *userService) updateUser(ctx context.Context, id string, email, username, firstName, lastName *string) (*ent.User, error) { update := s.client.User.UpdateOneID(id) if email != nil { exists, err := s.client.User.Query(). Where(user.Email(*email), user.IDNEQ(id)). Exist(ctx) if err != nil { return nil, fmt.Errorf("failed to check email: %w", err) } if exists { return nil, fmt.Errorf("email already taken") } update = update.SetEmail(*email) } if username != nil { update = update.SetUsername(*username) } if firstName != nil { update = update.SetFirstName(*firstName) } if lastName != nil { update = update.SetLastName(*lastName) } return update.Save(ctx) } // deleteUser deletes a user. func (s *userService) deleteUser(ctx context.Context, id string) error { return s.client.User.DeleteOneID(id).Exec(ctx) } // verifyEmail verifies a user's email. func (s *userService) verifyEmail(ctx context.Context, token string) error { u, err := s.client.User.Query().Where(user.EmailVerificationToken(token)).Only(ctx) if err != nil { return fmt.Errorf("invalid token") } _, err = s.client.User.UpdateOneID(u.ID). SetVerified(true). ClearEmailVerificationToken(). Save(ctx) return err } // requestPasswordReset requests a password reset. func (s *userService) requestPasswordReset(ctx context.Context, email string) (string, error) { u, err := s.getUserByEmail(ctx, email) if err != nil { return "", nil // Don't reveal if user exists } token, err := generateToken() if err != nil { return "", err } expiresAt := time.Now().Add(24 * time.Hour) _, err = s.client.User.UpdateOneID(u.ID). SetPasswordResetToken(token). SetPasswordResetExpiresAt(expiresAt). Save(ctx) return token, err } // resetPassword resets a password. func (s *userService) resetPassword(ctx context.Context, token, newPassword string) error { u, err := s.client.User.Query().Where(user.PasswordResetToken(token)).Only(ctx) if err != nil { return fmt.Errorf("invalid token") } if !u.PasswordResetExpiresAt.IsZero() && u.PasswordResetExpiresAt.Before(time.Now()) { return fmt.Errorf("token expired") } passwordHash, err := hashPassword(newPassword) if err != nil { return err } _, err = s.client.User.UpdateOneID(u.ID). SetPasswordHash(passwordHash). ClearPasswordResetToken(). ClearPasswordResetExpiresAt(). Save(ctx) return err } // verifyPassword verifies a password. func (s *userService) verifyPassword(ctx context.Context, email, pwd string) (*ent.User, error) { u, err := s.getUserByEmail(ctx, email) if err != nil { return nil, err } valid, err := verifyPassword(pwd, u.PasswordHash) if err != nil || !valid { return nil, fmt.Errorf("invalid password") } return u, nil } // identityServerImpl implements the IdentityService gRPC server. type identityServerImpl struct { identityv1.UnimplementedIdentityServiceServer service *userService logger *zap.Logger } // GetUser retrieves a user by ID. func (s *identityServerImpl) GetUser(ctx context.Context, req *identityv1.GetUserRequest) (*identityv1.GetUserResponse, error) { u, err := s.service.getUser(ctx, req.Id) if err != nil { return nil, status.Errorf(codes.NotFound, "user not found: %v", err) } return &identityv1.GetUserResponse{ User: &identityv1.User{ Id: u.ID, Email: u.Email, Username: u.Username, FirstName: u.FirstName, LastName: u.LastName, EmailVerified: u.Verified, CreatedAt: u.CreatedAt.Unix(), UpdatedAt: u.UpdatedAt.Unix(), }, }, nil } // GetUserByEmail retrieves a user by email. func (s *identityServerImpl) GetUserByEmail(ctx context.Context, req *identityv1.GetUserByEmailRequest) (*identityv1.GetUserByEmailResponse, error) { u, err := s.service.getUserByEmail(ctx, req.Email) if err != nil { return nil, status.Errorf(codes.NotFound, "user not found: %v", err) } return &identityv1.GetUserByEmailResponse{ User: &identityv1.User{ Id: u.ID, Email: u.Email, Username: u.Username, FirstName: u.FirstName, LastName: u.LastName, EmailVerified: u.Verified, CreatedAt: u.CreatedAt.Unix(), UpdatedAt: u.UpdatedAt.Unix(), }, }, nil } // CreateUser creates a new user. func (s *identityServerImpl) CreateUser(ctx context.Context, req *identityv1.CreateUserRequest) (*identityv1.CreateUserResponse, error) { u, err := s.service.createUser(ctx, req.Email, req.Username, req.Password, req.FirstName, req.LastName) if err != nil { return nil, status.Errorf(codes.Internal, "failed to create user: %v", err) } return &identityv1.CreateUserResponse{ User: &identityv1.User{ Id: u.ID, Email: u.Email, Username: u.Username, FirstName: u.FirstName, LastName: u.LastName, EmailVerified: u.Verified, CreatedAt: u.CreatedAt.Unix(), UpdatedAt: u.UpdatedAt.Unix(), }, }, nil } // UpdateUser updates a user. func (s *identityServerImpl) UpdateUser(ctx context.Context, req *identityv1.UpdateUserRequest) (*identityv1.UpdateUserResponse, error) { u, err := s.service.updateUser(ctx, req.Id, req.Email, req.Username, req.FirstName, req.LastName) if err != nil { return nil, status.Errorf(codes.Internal, "failed to update user: %v", err) } return &identityv1.UpdateUserResponse{ User: &identityv1.User{ Id: u.ID, Email: u.Email, Username: u.Username, FirstName: u.FirstName, LastName: u.LastName, EmailVerified: u.Verified, CreatedAt: u.CreatedAt.Unix(), UpdatedAt: u.UpdatedAt.Unix(), }, }, nil } // DeleteUser deletes a user. func (s *identityServerImpl) DeleteUser(ctx context.Context, req *identityv1.DeleteUserRequest) (*identityv1.DeleteUserResponse, error) { if err := s.service.deleteUser(ctx, req.Id); err != nil { return nil, status.Errorf(codes.Internal, "failed to delete user: %v", err) } return &identityv1.DeleteUserResponse{Success: true}, nil } // VerifyEmail verifies a user's email. func (s *identityServerImpl) VerifyEmail(ctx context.Context, req *identityv1.VerifyEmailRequest) (*identityv1.VerifyEmailResponse, error) { if err := s.service.verifyEmail(ctx, req.Token); err != nil { return nil, status.Errorf(codes.InvalidArgument, "failed to verify email: %v", err) } return &identityv1.VerifyEmailResponse{Success: true}, nil } // RequestPasswordReset requests a password reset. func (s *identityServerImpl) RequestPasswordReset(ctx context.Context, req *identityv1.RequestPasswordResetRequest) (*identityv1.RequestPasswordResetResponse, error) { _, err := s.service.requestPasswordReset(ctx, req.Email) if err != nil { return nil, status.Errorf(codes.Internal, "failed to request password reset: %v", err) } return &identityv1.RequestPasswordResetResponse{Success: true}, nil } // ResetPassword resets a password. func (s *identityServerImpl) ResetPassword(ctx context.Context, req *identityv1.ResetPasswordRequest) (*identityv1.ResetPasswordResponse, error) { if err := s.service.resetPassword(ctx, req.Token, req.NewPassword); err != nil { return nil, status.Errorf(codes.InvalidArgument, "failed to reset password: %v", err) } return &identityv1.ResetPasswordResponse{Success: true}, nil } // VerifyPassword verifies a user's password. func (s *identityServerImpl) VerifyPassword(ctx context.Context, req *identityv1.VerifyPasswordRequest) (*identityv1.VerifyPasswordResponse, error) { u, err := s.service.verifyPassword(ctx, req.Email, req.Password) if err != nil { return nil, status.Errorf(codes.Unauthenticated, "invalid credentials: %v", err) } return &identityv1.VerifyPasswordResponse{ User: &identityv1.User{ Id: u.ID, Email: u.Email, Username: u.Username, FirstName: u.FirstName, LastName: u.LastName, EmailVerified: u.Verified, CreatedAt: u.CreatedAt.Unix(), UpdatedAt: u.UpdatedAt.Unix(), }, }, nil } // provideIdentityService creates the identity service and gRPC server. func provideIdentityService() fx.Option { return fx.Options( // User service fx.Provide(func(client *database.Client, log logger.Logger) (*userService, error) { return &userService{ client: client, logger: log, }, nil }), // gRPC server implementation fx.Provide(func(userService *userService, log logger.Logger) (*identityServerImpl, error) { zapLogger, _ := zap.NewProduction() return &identityServerImpl{ service: userService, logger: zapLogger, }, nil }), // gRPC server wrapper fx.Provide(func( serverImpl *identityServerImpl, cfg config.ConfigProvider, log logger.Logger, ) (*grpcServerWrapper, error) { port := cfg.GetInt("services.identity.port") if port == 0 { port = 8082 } addr := fmt.Sprintf("0.0.0.0:%d", port) listener, err := net.Listen("tcp", addr) if err != nil { return nil, fmt.Errorf("failed to listen on %s: %w", addr, err) } grpcServer := grpc.NewServer() identityv1.RegisterIdentityServiceServer(grpcServer, serverImpl) // Register health service healthServer := health.NewServer() grpc_health_v1.RegisterHealthServer(grpcServer, healthServer) // Set serving status for the default service (empty string) - this is what Consul checks healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING) // Also set for the specific service name healthServer.SetServingStatus("identity.v1.IdentityService", grpc_health_v1.HealthCheckResponse_SERVING) // Register reflection for grpcurl reflection.Register(grpcServer) return &grpcServerWrapper{ server: grpcServer, listener: listener, port: port, logger: log, }, nil }), ) }