using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Dapr.Client; using FirebaseAdmin; using FirebaseAdmin.Messaging; using GB5Shared.DTO.PushNotification; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Login; using GB5Shared.Vault; using Google.Apis.Auth.OAuth2; namespace GB5Shared.PushNotification { public class PushNotificationDAL : IPushNotificationDAL { /// Vault KV v2 path holding the Firebase service-account JSON. Set via SetSecretEndpoint. private const string FirebaseServiceAccountVaultKey = "push-notification/firebase-service-account"; private static readonly SemaphoreSlim _initLock = new(1, 1); private static bool _firebaseInitialized = false; private readonly DaprClient _daprClient; private readonly IVaultService _vaultService; public PushNotificationDAL(IVaultService vaultService) { _daprClient = new DaprClientBuilder().Build(); _vaultService = vaultService; } #region Save User Token /// /// Saves a user's device token in the Dapr state store with a 24-hour TTL. /// /// User login details. /// Firebase device token. /// Status message indicating success or failure. public async Task SaveUserToken(LoginDTO loginDTO, string token) { try { string tokenCacheKey = $"UserToken:{loginDTO.UserId}:{loginDTO.ServerId}:{loginDTO.DatabaseName}:{loginDTO.LoginEventLogId}"; var tokenEntry = new UserTokenCache { Token = token, UserId = loginDTO.UserId, ServerId = loginDTO.ServerId, DatabaseName = loginDTO.DatabaseName, LoginEventLogId = loginDTO.LoginEventLogId, Expiration = DateTime.UtcNow.AddHours(24) }; var ttlMetadata = new Dictionary { { "ttlInSeconds", "86400" } }; // 24 hours await _daprClient.SaveStateAsync("pushnotification", tokenCacheKey, tokenEntry, metadata: ttlMetadata); // Track all user tokens for easy lookup later string userKeysKey = $"UserKeys:{loginDTO.UserId}"; var existingUserKeys = await _daprClient.GetStateAsync("pushnotification", userKeysKey) ?? new UserKeysCache(); if (!existingUserKeys.Keys.Contains(tokenCacheKey)) { existingUserKeys.Keys.Add(tokenCacheKey); await _daprClient.SaveStateAsync("pushnotification", userKeysKey, existingUserKeys); } return "User token saved successfully."; } catch (Exception) { throw; } } #endregion #region Subscribe to Topic /// /// Subscribes a user’s device token to a specific Firebase topic (e.g., “DEV”, “Payroll”). /// /// Device token. /// Topic name. /// Success or error message. public async Task SubscribeUserToTopic(string token, string topic) { try { await FirebaseInitializedAsync(); var tokens = new List { token }; var result = await FirebaseMessaging.DefaultInstance.SubscribeToTopicAsync(tokens, topic); if (result.FailureCount > 0) throw new Exception($"Subscription failed. Success: {result.SuccessCount}, Failure: {result.FailureCount}"); return $"User subscribed to topic '{topic}' successfully."; } catch (Exception) { throw; } } #endregion #region Send Notification to Topic /// /// Sends a push notification to all users subscribed to a specific Firebase topic. /// /// The notification data including topic, title, and body. /// /// A message indicating success with the Firebase message ID, or an error message on failure. /// public async Task SendNotificationToTopic(PushNotificationDTO pushNotificationDTO) { try { // Ensure Firebase app is initialized before sending await FirebaseInitializedAsync(); var message = new Message { Topic = pushNotificationDTO.NotificationTopic, //Notification = new Notification //{ // Title = pushNotificationDTO.NotificationTitle, // Body = pushNotificationDTO.NotificationBody //} }; // Send the message using Firebase Cloud Messaging string messageId = await FirebaseMessaging.DefaultInstance.SendAsync(message); return $"Notification sent successfully to topic '{pushNotificationDTO.NotificationTopic}'. Message ID: {messageId}"; } catch (Exception) { throw; } } #endregion #region Send Notification to Users (Single or Multiple) /// /// Sends a push notification to one or multiple users based on their stored tokens. /// /// /// The notification data including user IDs, title, and body. /// /// /// A message summarizing how many notifications were successfully sent or skipped. /// public async Task SendNotificationToUsers(PushNotificationDTO PushNotificationDTO) { try { await FirebaseInitializedAsync(); int totalSent = 0; foreach (int userId in PushNotificationDTO.UserIds) { string userKeysKey = $"UserKeys:{userId}"; var userTokenKeys = await _daprClient.GetStateAsync("pushnotification", userKeysKey); if (userTokenKeys == null || userTokenKeys.Keys.Count == 0) continue; foreach (var tokenKey in userTokenKeys.Keys) { var tokenEntry = await _daprClient.GetStateAsync("pushnotification", tokenKey); if (tokenEntry == null || tokenEntry.Expiration <= DateTime.UtcNow) continue; var message = new FirebaseAdmin.Messaging.Message { Token = tokenEntry.Token, Notification = new FirebaseAdmin.Messaging.Notification { Title = PushNotificationDTO.NotificationTitle, Body = PushNotificationDTO.NotificationBody } }; await FirebaseMessaging.DefaultInstance.SendAsync(message); totalSent++; } } return totalSent > 0 ? $"Notification sent to {totalSent} device(s)." : "No valid tokens found for the provided user(s)."; } catch (Exception) { throw; } } #endregion /// /// Lazily initializes the default from the service-account /// JSON stored in Vault at . Never hardcode /// the credential here — see for the only supported access path. /// private async Task FirebaseInitializedAsync() { if (_firebaseInitialized) return; await _initLock.WaitAsync().ConfigureAwait(false); try { if (_firebaseInitialized) return; string jsonCredentials = await _vaultService.GetSecretAsync(FirebaseServiceAccountVaultKey) .ConfigureAwait(false); FirebaseApp.Create(new AppOptions { Credential = GoogleCredential.FromJson(jsonCredentials) }); _firebaseInitialized = true; } finally { _initLock.Release(); } } } }