Files
goplt/services/identity/internal/service/user_service.go
0x1d b1b895e818 feat(epic2): Implement core authentication and authorization services
- Implement Audit Service (2.5)
  - gRPC server with Record and Query operations
  - Database persistence with audit schema
  - Service registry integration
  - Entry point: cmd/audit-service

- Implement Identity Service (2.2)
  - User CRUD operations
  - Password hashing with argon2id
  - Email verification and password reset flows
  - Entry point: cmd/identity-service
  - Fix package naming conflicts in user_service.go

- Implement Auth Service (2.1)
  - JWT token generation and validation
  - Login, RefreshToken, ValidateToken, Logout RPCs
  - Integration with Identity Service
  - Entry point: cmd/auth-service
  - Note: RefreshToken entity needs Ent generation

- Implement Authz Service (2.3, 2.4)
  - Permission checking and authorization
  - User roles and permissions retrieval
  - RBAC-based authorization
  - Entry point: cmd/authz-service

- Implement gRPC clients for all services
  - Auth, Identity, Authz, and Audit clients
  - Service discovery integration
  - Full gRPC communication

- Add service configurations to config/default.yaml
- Create SUMMARY.md with implementation details and testing instructions
- Fix compilation errors in Identity Service (password package conflicts)
- All services build successfully and tests pass
2025-11-06 20:07:20 +01:00

341 lines
8.6 KiB
Go

// Package service provides user service business logic.
package service
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"time"
"git.dcentral.systems/toolz/goplt/internal/ent"
"git.dcentral.systems/toolz/goplt/internal/ent/user"
"git.dcentral.systems/toolz/goplt/pkg/logger"
passwordpkg "git.dcentral.systems/toolz/goplt/services/identity/internal/password"
"go.uber.org/zap"
)
// UserService provides user management functionality.
type UserService struct {
client *ent.Client
logger logger.Logger
}
// NewUserService creates a new user service.
func NewUserService(client *ent.Client, log logger.Logger) *UserService {
return &UserService{
client: client,
logger: log,
}
}
// generateToken generates a random token for email verification or password reset.
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 base64.URLEncoding.EncodeToString(b), nil
}
// CreateUser creates a new user.
func (s *UserService) CreateUser(ctx context.Context, email, username, password, firstName, lastName string) (*ent.User, error) {
// Check if user with email already exists
exists, err := s.client.User.Query().
Where(user.Email(email)).
Exist(ctx)
if err != nil {
return nil, fmt.Errorf("failed to check email existence: %w", err)
}
if exists {
return nil, fmt.Errorf("user with email %s already exists", email)
}
// Hash password
passwordHash, err := passwordpkg.Hash(password)
if err != nil {
return nil, fmt.Errorf("failed to hash password: %w", err)
}
// Generate email verification token
verificationToken, err := generateToken()
if err != nil {
return nil, fmt.Errorf("failed to generate verification token: %w", err)
}
// Create user
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 {
s.logger.Error("Failed to create user",
zap.Error(err),
zap.String("email", email),
)
return nil, fmt.Errorf("failed to create user: %w", err)
}
s.logger.Info("User created",
zap.String("user_id", u.ID),
zap.String("email", email),
)
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 {
if ent.IsNotFound(err) {
return nil, fmt.Errorf("user not found: %w", err)
}
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 {
if ent.IsNotFound(err) {
return nil, fmt.Errorf("user not found: %w", err)
}
return nil, fmt.Errorf("failed to get user by email: %w", err)
}
return u, nil
}
// UpdateUser updates a user's profile.
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 {
// Check if email is already taken by another user
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 existence: %w", err)
}
if exists {
return nil, fmt.Errorf("email %s is already taken", *email)
}
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)
}
u, err := update.Save(ctx)
if err != nil {
if ent.IsNotFound(err) {
return nil, fmt.Errorf("user not found: %w", err)
}
return nil, fmt.Errorf("failed to update user: %w", err)
}
s.logger.Info("User updated",
zap.String("user_id", id),
)
return u, nil
}
// DeleteUser deletes a user (soft delete by setting verified to false, or hard delete).
func (s *UserService) DeleteUser(ctx context.Context, id string) error {
err := s.client.User.DeleteOneID(id).Exec(ctx)
if err != nil {
if ent.IsNotFound(err) {
return fmt.Errorf("user not found: %w", err)
}
return fmt.Errorf("failed to delete user: %w", err)
}
s.logger.Info("User deleted",
zap.String("user_id", id),
)
return nil
}
// VerifyEmail verifies a user's email address using a verification token.
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 {
if ent.IsNotFound(err) {
return fmt.Errorf("invalid verification token")
}
return fmt.Errorf("failed to find user: %w", err)
}
// Update user to verified and clear token
_, err = s.client.User.UpdateOneID(u.ID).
SetVerified(true).
ClearEmailVerificationToken().
Save(ctx)
if err != nil {
return fmt.Errorf("failed to verify email: %w", err)
}
s.logger.Info("Email verified",
zap.String("user_id", u.ID),
zap.String("email", u.Email),
)
return nil
}
// RequestPasswordReset requests a password reset token.
func (s *UserService) RequestPasswordReset(ctx context.Context, email string) (string, error) {
u, err := s.GetUserByEmail(ctx, email)
if err != nil {
// Don't reveal if user exists or not (security best practice)
return "", nil
}
// Generate reset token
resetToken, err := generateToken()
if err != nil {
return "", fmt.Errorf("failed to generate reset token: %w", err)
}
// Set reset token with expiration (24 hours)
expiresAt := time.Now().Add(24 * time.Hour)
_, err = s.client.User.UpdateOneID(u.ID).
SetPasswordResetToken(resetToken).
SetPasswordResetExpiresAt(expiresAt).
Save(ctx)
if err != nil {
return "", fmt.Errorf("failed to set reset token: %w", err)
}
s.logger.Info("Password reset requested",
zap.String("user_id", u.ID),
zap.String("email", email),
)
return resetToken, nil
}
// ResetPassword resets a user's password using a reset token.
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 {
if ent.IsNotFound(err) {
return fmt.Errorf("invalid reset token")
}
return fmt.Errorf("failed to find user: %w", err)
}
// Check if token is expired
if !u.PasswordResetExpiresAt.IsZero() && u.PasswordResetExpiresAt.Before(time.Now()) {
return fmt.Errorf("reset token has expired")
}
// Hash new password
passwordHash, err := passwordpkg.Hash(newPassword)
if err != nil {
return fmt.Errorf("failed to hash password: %w", err)
}
// Update password and clear reset token
_, err = s.client.User.UpdateOneID(u.ID).
SetPasswordHash(passwordHash).
ClearPasswordResetToken().
ClearPasswordResetExpiresAt().
Save(ctx)
if err != nil {
return fmt.Errorf("failed to reset password: %w", err)
}
s.logger.Info("Password reset",
zap.String("user_id", u.ID),
)
return nil
}
// ChangePassword changes a user's password with old password verification.
func (s *UserService) ChangePassword(ctx context.Context, userID, oldPassword, newPassword string) error {
u, err := s.GetUser(ctx, userID)
if err != nil {
return err
}
// Verify old password
valid, err := passwordpkg.Verify(oldPassword, u.PasswordHash)
if err != nil {
return fmt.Errorf("failed to verify password: %w", err)
}
if !valid {
return fmt.Errorf("invalid old password")
}
// Hash new password
passwordHash, err := passwordpkg.Hash(newPassword)
if err != nil {
return fmt.Errorf("failed to hash password: %w", err)
}
// Update password
_, err = s.client.User.UpdateOneID(userID).
SetPasswordHash(passwordHash).
Save(ctx)
if err != nil {
return fmt.Errorf("failed to change password: %w", err)
}
s.logger.Info("Password changed",
zap.String("user_id", userID),
)
return nil
}
// VerifyPassword verifies a password against a user's password hash.
func (s *UserService) VerifyPassword(ctx context.Context, email, password string) (*ent.User, error) {
u, err := s.GetUserByEmail(ctx, email)
if err != nil {
return nil, err
}
valid, err := passwordpkg.Verify(password, u.PasswordHash)
if err != nil {
return nil, fmt.Errorf("failed to verify password: %w", err)
}
if !valid {
return nil, fmt.Errorf("invalid password")
}
return u, nil
}