monorepo/cloud/maplepress-backend/internal/service/gateway/login.go

165 lines
4.9 KiB
Go

package gateway
import (
"context"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/service"
gatewayuc "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/gateway"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/jwt"
)
// LoginService handles user login operations
type LoginService interface {
Login(ctx context.Context, input *LoginInput) (*LoginResponse, error)
}
// LoginInput represents the input for user login
type LoginInput struct {
Email string
Password string
}
// LoginResponse represents the response after successful login
type LoginResponse struct {
// User details
UserID string `json:"user_id"`
UserEmail string `json:"user_email"`
UserName string `json:"user_name"`
UserRole string `json:"user_role"`
// Tenant details
TenantID string `json:"tenant_id"`
// Session and tokens
SessionID string `json:"session_id"`
AccessToken string `json:"access_token"`
AccessExpiry time.Time `json:"access_expiry"`
RefreshToken string `json:"refresh_token"`
RefreshExpiry time.Time `json:"refresh_expiry"`
LoginAt time.Time `json:"login_at"`
}
type loginService struct {
loginUC *gatewayuc.LoginUseCase
sessionService service.SessionService
jwtProvider jwt.Provider
logger *zap.Logger
}
// NewLoginService creates a new login service
func NewLoginService(
loginUC *gatewayuc.LoginUseCase,
sessionService service.SessionService,
jwtProvider jwt.Provider,
logger *zap.Logger,
) LoginService {
return &loginService{
loginUC: loginUC,
sessionService: sessionService,
jwtProvider: jwtProvider,
logger: logger.Named("login-service"),
}
}
// Login handles the complete login flow
func (s *loginService) Login(ctx context.Context, input *LoginInput) (*LoginResponse, error) {
// CWE-532: Use hashed email to prevent PII in logs
s.logger.Info("processing login request",
logger.EmailHash(input.Email))
// Execute login use case (validates credentials)
loginOutput, err := s.loginUC.Execute(ctx, &gatewayuc.LoginInput{
Email: input.Email,
Password: input.Password,
})
if err != nil {
s.logger.Error("login failed", zap.Error(err))
return nil, err
}
// CWE-532: Use hashed email to prevent PII in logs
s.logger.Info("credentials validated successfully",
zap.String("user_id", loginOutput.UserID),
logger.EmailHash(loginOutput.UserEmail),
zap.String("tenant_id", loginOutput.TenantID))
// Parse tenant ID to UUID
tenantUUID, err := uuid.Parse(loginOutput.TenantID)
if err != nil {
s.logger.Error("failed to parse tenant ID", zap.Error(err))
return nil, err
}
// Parse user ID to UUID
userUUID, err := uuid.Parse(loginOutput.UserID)
if err != nil {
s.logger.Error("failed to parse user ID", zap.Error(err))
return nil, err
}
// CWE-384: Invalidate all existing sessions before creating new one (Session Fixation Prevention)
// This ensures that any session IDs an attacker may have obtained are invalidated
s.logger.Info("invalidating existing sessions for security",
zap.String("user_uuid", userUUID.String()))
if err := s.sessionService.InvalidateUserSessions(ctx, userUUID); err != nil {
// Log warning but don't fail login - this is best effort cleanup
s.logger.Warn("failed to invalidate existing sessions (non-fatal)",
zap.String("user_uuid", userUUID.String()),
zap.Error(err))
}
// Create new session in two-tier cache
session, err := s.sessionService.CreateSession(
ctx,
0, // UserID as uint64 - not used in our UUID-based system
userUUID,
loginOutput.UserEmail,
loginOutput.UserName,
loginOutput.UserRole,
tenantUUID,
)
if err != nil {
s.logger.Error("failed to create session", zap.Error(err))
return nil, err
}
s.logger.Info("session created", zap.String("session_id", session.ID))
// Generate JWT access and refresh tokens
accessToken, accessExpiry, refreshToken, refreshExpiry, err := s.jwtProvider.GenerateTokenPair(
session.ID,
AccessTokenDuration,
RefreshTokenDuration,
)
if err != nil {
s.logger.Error("failed to generate tokens", zap.Error(err))
// Clean up session
_ = s.sessionService.DeleteSession(ctx, session.ID)
return nil, err
}
s.logger.Info("login completed successfully",
zap.String("user_id", loginOutput.UserID),
zap.String("tenant_id", loginOutput.TenantID),
zap.String("session_id", session.ID))
return &LoginResponse{
UserID: loginOutput.UserID,
UserEmail: loginOutput.UserEmail,
UserName: loginOutput.UserName,
UserRole: loginOutput.UserRole,
TenantID: loginOutput.TenantID,
SessionID: session.ID,
AccessToken: accessToken,
AccessExpiry: accessExpiry,
RefreshToken: refreshToken,
RefreshExpiry: refreshExpiry,
LoginAt: time.Now().UTC(),
}, nil
}