|
| 1 | +import 'package:ht_data_repository/ht_data_repository.dart'; |
| 2 | +import 'package:ht_email_repository/ht_email_repository.dart'; |
| 3 | +import 'package:ht_shared/ht_shared.dart'; |
| 4 | +import 'package:uuid/uuid.dart'; |
| 5 | + |
| 6 | +import 'package:ht_api/src/services/auth_token_service.dart'; |
| 7 | +import 'package:ht_api/src/services/verification_code_storage_service.dart'; |
| 8 | + |
| 9 | +/// {@template auth_service} |
| 10 | +/// Service responsible for orchestrating authentication logic on the backend. |
| 11 | +/// |
| 12 | +/// It coordinates interactions between user data storage, token generation, |
| 13 | +/// verification code management, and email sending. |
| 14 | +/// {@endtemplate} |
| 15 | +class AuthService { |
| 16 | + /// {@macro auth_service} |
| 17 | + const AuthService({ |
| 18 | + required HtDataRepository<User> userRepository, |
| 19 | + required AuthTokenService authTokenService, |
| 20 | + required VerificationCodeStorageService verificationCodeStorageService, |
| 21 | + required HtEmailRepository emailRepository, |
| 22 | + required Uuid uuidGenerator, |
| 23 | + }) : _userRepository = userRepository, |
| 24 | + _authTokenService = authTokenService, |
| 25 | + _verificationCodeStorageService = verificationCodeStorageService, |
| 26 | + _emailRepository = emailRepository, |
| 27 | + _uuid = uuidGenerator; |
| 28 | + |
| 29 | + final HtDataRepository<User> _userRepository; |
| 30 | + final AuthTokenService _authTokenService; |
| 31 | + final VerificationCodeStorageService _verificationCodeStorageService; |
| 32 | + final HtEmailRepository _emailRepository; |
| 33 | + final Uuid _uuid; |
| 34 | + |
| 35 | + /// Initiates the email sign-in process. |
| 36 | + /// |
| 37 | + /// Generates a verification code, stores it, and sends it via email. |
| 38 | + /// Throws [InvalidInputException] for invalid email format (via email client). |
| 39 | + /// Throws [OperationFailedException] if code generation/storage/email fails. |
| 40 | + Future<void> initiateEmailSignIn(String email) async { |
| 41 | + try { |
| 42 | + // Generate and store the code |
| 43 | + final code = await _verificationCodeStorageService.generateAndStoreCode( |
| 44 | + email, |
| 45 | + ); |
| 46 | + |
| 47 | + // Send the code via email |
| 48 | + await _emailRepository.sendOtpEmail( |
| 49 | + recipientEmail: email, |
| 50 | + otpCode: code, |
| 51 | + ); |
| 52 | + print('Initiated email sign-in for $email, code sent.'); |
| 53 | + } on HtHttpException { |
| 54 | + // Propagate known exceptions from dependencies |
| 55 | + rethrow; |
| 56 | + } catch (e) { |
| 57 | + // Catch unexpected errors during orchestration |
| 58 | + print('Error during initiateEmailSignIn for $email: $e'); |
| 59 | + throw const OperationFailedException( |
| 60 | + 'Failed to initiate email sign-in process.', |
| 61 | + ); |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + /// Completes the email sign-in process by verifying the code. |
| 66 | + /// |
| 67 | + /// If the code is valid, finds or creates the user, generates an auth token. |
| 68 | + /// Returns the authenticated User and the generated token. |
| 69 | + /// Throws [InvalidInputException] if the code is invalid or expired. |
| 70 | + /// Throws [AuthenticationException] for specific code mismatch. |
| 71 | + /// Throws [OperationFailedException] for user lookup/creation or token errors. |
| 72 | + Future<({User user, String token})> completeEmailSignIn( |
| 73 | + String email, |
| 74 | + String code, |
| 75 | + ) async { |
| 76 | + // 1. Validate the code |
| 77 | + final isValidCode = await _verificationCodeStorageService.validateCode( |
| 78 | + email, |
| 79 | + code, |
| 80 | + ); |
| 81 | + if (!isValidCode) { |
| 82 | + // Consider distinguishing between expired and simply incorrect codes |
| 83 | + // For now, treat both as invalid input. |
| 84 | + throw const InvalidInputException( |
| 85 | + 'Invalid or expired verification code.',); |
| 86 | + } |
| 87 | + |
| 88 | + // 2. Find or create the user |
| 89 | + User user; |
| 90 | + try { |
| 91 | + // Attempt to find user by email (assuming a query method exists) |
| 92 | + // NOTE: HtDataRepository<User> currently lacks findByEmail. |
| 93 | + // We'll simulate this by querying all and filtering for now. |
| 94 | + // Replace with a proper query when available. |
| 95 | + final query = {'email': email}; // Hypothetical query |
| 96 | + final paginatedResponse = await _userRepository.readAllByQuery(query); |
| 97 | + |
| 98 | + if (paginatedResponse.items.isNotEmpty) { |
| 99 | + user = paginatedResponse.items.first; |
| 100 | + print('Found existing user: ${user.id} for email $email'); |
| 101 | + } else { |
| 102 | + // User not found, create a new one |
| 103 | + print('User not found for $email, creating new user.'); |
| 104 | + user = User( |
| 105 | + id: _uuid.v4(), // Generate new ID |
| 106 | + email: email, |
| 107 | + isAnonymous: false, // Email verified user is not anonymous |
| 108 | + ); |
| 109 | + user = await _userRepository.create(user); // Save the new user |
| 110 | + print('Created new user: ${user.id}'); |
| 111 | + } |
| 112 | + } on HtHttpException catch (e) { |
| 113 | + print('Error finding/creating user for $email: $e'); |
| 114 | + throw const OperationFailedException( |
| 115 | + 'Failed to find or create user account.',); |
| 116 | + } catch (e) { |
| 117 | + print('Unexpected error during user lookup/creation for $email: $e'); |
| 118 | + throw const OperationFailedException('Failed to process user account.'); |
| 119 | + } |
| 120 | + |
| 121 | + // 3. Generate authentication token |
| 122 | + try { |
| 123 | + final token = await _authTokenService.generateToken(user); |
| 124 | + print('Generated token for user ${user.id}'); |
| 125 | + return (user: user, token: token); |
| 126 | + } catch (e) { |
| 127 | + print('Error generating token for user ${user.id}: $e'); |
| 128 | + throw const OperationFailedException( |
| 129 | + 'Failed to generate authentication token.',); |
| 130 | + } |
| 131 | + } |
| 132 | + |
| 133 | + /// Performs anonymous sign-in. |
| 134 | + /// |
| 135 | + /// Creates a new anonymous user record and generates an auth token. |
| 136 | + /// Returns the anonymous User and the generated token. |
| 137 | + /// Throws [OperationFailedException] if user creation or token generation fails. |
| 138 | + Future<({User user, String token})> performAnonymousSignIn() async { |
| 139 | + // 1. Create anonymous user |
| 140 | + User user; |
| 141 | + try { |
| 142 | + user = User( |
| 143 | + id: _uuid.v4(), // Generate new ID |
| 144 | + isAnonymous: true, |
| 145 | + email: null, // Anonymous users don't have an email initially |
| 146 | + ); |
| 147 | + user = await _userRepository.create(user); |
| 148 | + print('Created anonymous user: ${user.id}'); |
| 149 | + } on HtHttpException catch (e) { |
| 150 | + print('Error creating anonymous user: $e'); |
| 151 | + throw const OperationFailedException('Failed to create anonymous user.'); |
| 152 | + } catch (e) { |
| 153 | + print('Unexpected error during anonymous user creation: $e'); |
| 154 | + throw const OperationFailedException( |
| 155 | + 'Failed to process anonymous sign-in.',); |
| 156 | + } |
| 157 | + |
| 158 | + // 2. Generate token |
| 159 | + try { |
| 160 | + final token = await _authTokenService.generateToken(user); |
| 161 | + print('Generated token for anonymous user ${user.id}'); |
| 162 | + return (user: user, token: token); |
| 163 | + } catch (e) { |
| 164 | + print('Error generating token for anonymous user ${user.id}: $e'); |
| 165 | + throw const OperationFailedException( |
| 166 | + 'Failed to generate authentication token.',); |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + /// Performs sign-out actions (currently placeholder). |
| 171 | + /// |
| 172 | + /// In a real implementation, this might involve invalidating the token |
| 173 | + /// on the server-side (e.g., adding to a blacklist if using JWTs). |
| 174 | + /// The primary sign-out action (clearing local token) happens client-side. |
| 175 | + Future<void> performSignOut({required String userId}) async { |
| 176 | + // Placeholder: Server-side token invalidation logic would go here. |
| 177 | + // For the current SimpleAuthTokenService, there's nothing server-side |
| 178 | + // to invalidate easily. A real JWT implementation might use a blacklist. |
| 179 | + print('Performing server-side sign-out actions for user $userId (if any).'); |
| 180 | + await Future<void>.delayed(Duration.zero); // Simulate async |
| 181 | + // No exceptions thrown here unless invalidation fails. |
| 182 | + } |
| 183 | +} |
0 commit comments