monorepo/native/desktop/maplefile/internal/service/auth/service.go

281 lines
9.2 KiB
Go

package auth
import (
"context"
"time"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/maplefile/client"
domainSession "codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/domain/session"
"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/usecase/session"
"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/utils"
)
type Service struct {
apiClient *client.Client
createSessionUC *session.CreateUseCase
getSessionUC *session.GetByIdUseCase
deleteSessionUC *session.DeleteUseCase
saveSessionUC *session.SaveUseCase
logger *zap.Logger
}
// ProvideService creates the auth service for Wire
func ProvideService(
apiClient *client.Client,
createSessionUC *session.CreateUseCase,
getSessionUC *session.GetByIdUseCase,
deleteSessionUC *session.DeleteUseCase,
saveSessionUC *session.SaveUseCase,
logger *zap.Logger,
) *Service {
svc := &Service{
apiClient: apiClient,
createSessionUC: createSessionUC,
getSessionUC: getSessionUC,
deleteSessionUC: deleteSessionUC,
saveSessionUC: saveSessionUC,
logger: logger.Named("auth-service"),
}
// Set up token refresh callback to persist new tokens to session
apiClient.OnTokenRefresh(func(accessToken, refreshToken, accessTokenExpiryDate string) {
svc.handleTokenRefresh(accessToken, refreshToken, accessTokenExpiryDate)
})
return svc
}
// handleTokenRefresh is called when the API client automatically refreshes the access token
func (s *Service) handleTokenRefresh(accessToken, refreshToken, accessTokenExpiryDate string) {
// Get the current session
existingSession, err := s.getSessionUC.Execute()
if err != nil {
s.logger.Error("Failed to get session during token refresh callback", zap.Error(err))
return
}
if existingSession == nil {
s.logger.Warn("No session found during token refresh callback")
return
}
// Update the session with new tokens
existingSession.AccessToken = accessToken
existingSession.RefreshToken = refreshToken
// Parse the actual expiry date from the response instead of using hardcoded value
if accessTokenExpiryDate != "" {
expiryTime, parseErr := time.Parse(time.RFC3339, accessTokenExpiryDate)
if parseErr != nil {
s.logger.Warn("Failed to parse access token expiry date, using default 15m",
zap.String("expiry_date", accessTokenExpiryDate),
zap.Error(parseErr))
existingSession.ExpiresAt = time.Now().Add(15 * time.Minute)
} else {
existingSession.ExpiresAt = expiryTime
s.logger.Debug("Using actual token expiry from response",
zap.Time("expiry_time", expiryTime))
}
} else {
s.logger.Warn("No access token expiry date in refresh response, using default 15m")
existingSession.ExpiresAt = time.Now().Add(15 * time.Minute)
}
// Save updated session
if err := s.saveSessionUC.Execute(existingSession); err != nil {
s.logger.Error("Failed to save session after token refresh", zap.Error(err))
return
}
s.logger.Info("Session updated with refreshed tokens", zap.String("email", utils.MaskEmail(existingSession.Email)))
}
// RequestOTT requests a one-time token for login
func (s *Service) RequestOTT(ctx context.Context, email string) error {
_, err := s.apiClient.RequestOTT(ctx, email)
if err != nil {
s.logger.Error("Failed to request OTT", zap.Error(err))
return err
}
s.logger.Info("OTT requested successfully", zap.String("email", utils.MaskEmail(email)))
return nil
}
// VerifyOTT verifies the one-time token and returns the encrypted challenge
func (s *Service) VerifyOTT(ctx context.Context, email, ott string) (*client.VerifyOTTResponse, error) {
resp, err := s.apiClient.VerifyOTT(ctx, email, ott)
if err != nil {
s.logger.Error("OTT verification failed", zap.Error(err))
return nil, err
}
s.logger.Info("OTT verified successfully", zap.String("email", utils.MaskEmail(email)))
return resp, nil
}
// CompleteLogin completes the login process with OTT and challenge
func (s *Service) CompleteLogin(ctx context.Context, input *client.CompleteLoginInput) (*client.LoginResponse, error) {
// Complete login via API
resp, err := s.apiClient.CompleteLogin(ctx, input)
if err != nil {
s.logger.Error("Login failed", zap.Error(err))
return nil, err
}
// Parse expiration time from response
var expiresIn time.Duration
if resp.AccessTokenExpiryDate != "" {
expiryTime, parseErr := time.Parse(time.RFC3339, resp.AccessTokenExpiryDate)
if parseErr != nil {
s.logger.Warn("Failed to parse access token expiry date, using default 15m",
zap.String("expiry_date", resp.AccessTokenExpiryDate),
zap.Error(parseErr))
expiresIn = 15 * time.Minute // Default to 15 minutes (backend default)
} else {
expiresIn = time.Until(expiryTime)
s.logger.Info("Parsed access token expiry",
zap.Time("expiry_time", expiryTime),
zap.Duration("expires_in", expiresIn))
}
} else {
s.logger.Warn("No access token expiry date in response, using default 15m")
expiresIn = 15 * time.Minute // Default to 15 minutes (backend default)
}
// Use email as userID for now (can be improved later)
userID := input.Email
// Save session locally via use case
err = s.createSessionUC.Execute(
userID,
input.Email,
resp.AccessToken,
resp.RefreshToken,
expiresIn,
)
if err != nil {
s.logger.Error("Failed to save session", zap.Error(err))
return nil, err
}
s.logger.Info("User logged in successfully", zap.String("email", utils.MaskEmail(input.Email)))
return resp, nil
}
// Logout removes the local session
func (s *Service) Logout(ctx context.Context) error {
// Delete local session
err := s.deleteSessionUC.Execute()
if err != nil {
s.logger.Error("Failed to delete session", zap.Error(err))
return err
}
s.logger.Info("User logged out successfully")
return nil
}
// GetCurrentSession retrieves the current user session
func (s *Service) GetCurrentSession(ctx context.Context) (*domainSession.Session, error) {
sess, err := s.getSessionUC.Execute()
if err != nil {
s.logger.Error("Failed to get session", zap.Error(err))
return nil, err
}
return sess, nil
}
// UpdateSession updates the current session
func (s *Service) UpdateSession(ctx context.Context, sess *domainSession.Session) error {
return s.saveSessionUC.Execute(sess)
}
// IsLoggedIn checks if a user is currently logged in
func (s *Service) IsLoggedIn(ctx context.Context) (bool, error) {
sess, err := s.getSessionUC.Execute()
if err != nil {
return false, err
}
if sess == nil {
return false, nil
}
return sess.IsValid(), nil
}
// RestoreSession restores tokens to the API client from a persisted session
// This is used on app startup to resume a session from a previous run
func (s *Service) RestoreSession(ctx context.Context, sess *domainSession.Session) error {
if sess == nil {
return nil
}
// Restore tokens to API client
s.apiClient.SetTokens(sess.AccessToken, sess.RefreshToken)
s.logger.Info("Session restored to API client",
zap.String("user_id", sess.UserID),
zap.String("email", utils.MaskEmail(sess.Email)))
return nil
}
// Register creates a new user account
func (s *Service) Register(ctx context.Context, input *client.RegisterInput) error {
_, err := s.apiClient.Register(ctx, input)
if err != nil {
s.logger.Error("Registration failed", zap.Error(err))
return err
}
s.logger.Info("User registered successfully", zap.String("email", utils.MaskEmail(input.Email)))
return nil
}
// VerifyEmail verifies the email with the verification code
func (s *Service) VerifyEmail(ctx context.Context, input *client.VerifyEmailInput) error {
_, err := s.apiClient.VerifyEmailCode(ctx, input)
if err != nil {
s.logger.Error("Email verification failed", zap.Error(err))
return err
}
s.logger.Info("Email verified successfully", zap.String("email", utils.MaskEmail(input.Email)))
return nil
}
// GetAPIClient returns the API client instance
// This allows other parts of the application to make authenticated API calls
func (s *Service) GetAPIClient() *client.Client {
return s.apiClient
}
// InitiateRecovery initiates the account recovery process
func (s *Service) InitiateRecovery(ctx context.Context, email, method string) (*client.RecoveryInitiateResponse, error) {
resp, err := s.apiClient.RecoveryInitiate(ctx, email, method)
if err != nil {
s.logger.Error("Recovery initiation failed", zap.Error(err))
return nil, err
}
s.logger.Info("Recovery initiated successfully", zap.String("email", utils.MaskEmail(email)))
return resp, nil
}
// VerifyRecovery verifies the recovery challenge
func (s *Service) VerifyRecovery(ctx context.Context, input *client.RecoveryVerifyInput) (*client.RecoveryVerifyResponse, error) {
resp, err := s.apiClient.RecoveryVerify(ctx, input)
if err != nil {
s.logger.Error("Recovery verification failed", zap.Error(err))
return nil, err
}
s.logger.Info("Recovery verification successful")
return resp, nil
}
// CompleteRecovery completes the account recovery and resets credentials
func (s *Service) CompleteRecovery(ctx context.Context, input *client.RecoveryCompleteInput) (*client.RecoveryCompleteResponse, error) {
resp, err := s.apiClient.RecoveryComplete(ctx, input)
if err != nil {
s.logger.Error("Recovery completion failed", zap.Error(err))
return nil, err
}
s.logger.Info("Recovery completed successfully")
return resp, nil
}