fix(services): Fix service startup failures
- Remove duplicate CoreModule() calls from all service main.go files - NewContainer() already includes CoreModule() automatically - This was causing duplicate ConfigProvider provider errors - Update all _fx.go files to use *database.Client instead of *ent.Client - database.Client embeds *ent.Client, so it can be used directly - This fixes type mismatches between providers and consumers - Keep ent import for constants like ent.Desc - All services now build and should start successfully
This commit is contained in:
414
cmd/identity-service/identity_service_fx.go
Normal file
414
cmd/identity-service/identity_service_fx.go
Normal file
@@ -0,0 +1,414 @@
|
||||
// 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"
|
||||
"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
|
||||
}
|
||||
actualHash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, uint32(len(expectedHash)))
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
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
|
||||
}),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user