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 }