private async Task <Result> SnoozeOrDismissAsync(int participantId, DateTimeOffset clientTime, bool dismiss) { var typeName = dismiss ? "dismiss" : "snooze"; var participant = await _db.Participants .Include(x => x.Activity) .Include(x => x.User) .Include(x => x.NotificationSettings) .SingleOrDefaultAsync(x => x.Id == participantId); if (participant is null) { var message = $"Invalid participant ID {participantId}"; _logger.LogError(message); return(Result.Failure(message)); } var now = DateTimeOffset.UtcNow; if (participant.Activity.Due <= now) { var snoozeTime = clientTime.AddHours(Math.Max((byte)1, participant.User.SnoozeHours)); var nextCheck = snoozeTime; // base calculations on the client time to have the proper timezone and to account for request delays, but check for big differences var diff = (now - clientTime).Duration(); if (diff > TimeSpan.FromHours(1)) { _logger.LogWarning($"Client time {clientTime} differs from current time {now} by {diff}"); } if (dismiss) { // dismiss until the dismissal time of day on the current day in the client's timezone nextCheck = new DateTimeOffset(clientTime.Date.Add(participant.DismissUntilTimeOfDay), clientTime.Offset); // dismiss until tomorrow if that time has already passed or we're within the snooze time if (snoozeTime >= nextCheck) { nextCheck = nextCheck.AddDays(1); } } _logger.LogInformation($"Fulfilling {typeName} notification for user {participant.UserId}, participant {participant.Id}, activity {participant.ActivityId}, from {clientTime} to {nextCheck}"); foreach (var notificationSetting in participant.NotificationSettings.Where(x => x.Type == NotificationType.OverdueAnybody || x.Type == NotificationType.OverdueMine)) { notificationSetting.NextCheck = nextCheck; } await _db.SaveChangesAsync(); } else { _logger.LogInformation($"Ignoring {typeName} notification for user {participant.UserId}, participant {participant.Id}, activity {participant.ActivityId}"); } return(Result.Success()); }
public async Task <Result> DeleteDevice(int deviceAuthorizationId) { try { var device = await _db.DeviceAuthorizations .Include(x => x.Logins) .SingleOrDefaultAsync(x => x.Id == deviceAuthorizationId); if (device != null) { _db.RemoveRange(device.Logins); _db.Remove(device); await _db.SaveChangesAsync(); } return(Result.Success()); } catch (Exception e) { _logger.LogError($"Failed to delete device {deviceAuthorizationId}", e); return(Result.Failure("Failed to delete device")); } }
private async Task PruneExpiredLogins(TurnContext db, DateTimeOffset now, CancellationToken stoppingToken) { try { stoppingToken.ThrowIfCancellationRequested(); _logger.LogInformation("Pruning expired logins"); var expiredLogins = db.Logins.Where(x => x.ExpirationDate <= now); db.Logins.RemoveRange(expiredLogins); var pruned = await db.SaveChangesAsync(stoppingToken); _logger.LogInformation($"Pruned {pruned} expired logins"); } catch (Exception e) { _logger.LogError(e, "Failed to prune expired logins"); } }
private async Task PruneInactiveDeviceAuthorizations(TurnContext db, DateTimeOffset now, CancellationToken stoppingToken) { try { stoppingToken.ThrowIfCancellationRequested(); _logger.LogInformation("Pruning inactive device authorizations"); var inactiveDate = now - _appSettings.Value.DeviceInactivityPeriod; var inactiveDevices = db.DeviceAuthorizations.Where(x => x.ModifiedDate < inactiveDate); db.DeviceAuthorizations.RemoveRange(inactiveDevices); var pruned = await db.SaveChangesAsync(stoppingToken); _logger.LogInformation($"Pruned {pruned} device authorizations older than {inactiveDate}"); } catch (Exception e) { _logger.LogError(e, "Failed to prune inactive device authorizations"); } }
public async Task <Result> UpdateDismissTimeOfDayAsync(int participantId, TimeSpan time) { try { var participant = await _db.Participants.FindAsync(participantId); if (participant != null) { participant.DismissUntilTimeOfDay = time; await _db.SaveChangesAsync(); } return(Result.Success()); } catch (Exception e) { _logger.LogError(e, $"Failed to update dismiss time of day to {time} for participant {participantId}"); return(Result.Failure(e.Message)); } }
public async Task <Result> RemoveSubscriptionAsync(int userId, PushSubscription sub, bool save) { try { var device = await _db.PushSubscriptionDevices.FindAsync(userId, sub.Endpoint); if (device != null) { _db.PushSubscriptionDevices.Remove(device); if (save) { await _db.SaveChangesAsync(); } } return(Result.Success()); } catch (Exception e) { _logger.LogError(e, $"Failed to delete sub for user {userId}"); return(Result.Failure("Failed to delete subscription")); } }
public async Task <Result <Fido2.CredentialMakeResult> > MakeCredentialAsync(AuthenticatorAttestationRawResponse attestationResponse, int userId, long loginId, string deviceName) { try { // 1. get the options we sent the client var cacheKey = $"CredentialOptions:{loginId}"; if (!_cache.TryGetValue(cacheKey, out CredentialCreateOptions options)) { _logger.LogError($"Failed to find credential options for user {userId} login {loginId}"); return(Result.Failure <Fido2.CredentialMakeResult>("No challenge found")); } _cache.Remove(cacheKey); // 2. Verify and make the credentials var cmr = await _fido2.MakeNewCredentialAsync(attestationResponse, options, x => Task.FromResult(true)); // 3. Store the credentials in db _db.DeviceAuthorizations.Add(new DeviceAuthorization { UserId = userId, PublicKey = cmr.Result.PublicKey, CredentialId = cmr.Result.CredentialId, SignatureCounter = cmr.Result.Counter, DeviceName = deviceName }); await _db.SaveChangesAsync(); // 4. return "ok" to the client _logger.LogInformation($"Created credential for user {userId} login {loginId}"); return(Result.Success(cmr)); } catch (Exception e) { _logger.LogError(e, $"Failed to make credential for user {userId} login {loginId}"); return(Result.Failure <Fido2.CredentialMakeResult>("Error making credential")); } }
public async Task <Result <ActivityDetails, TurnError> > TakeTurnAsync(DateTimeOffset activityModifiedDate, int activityId, int byUserId, int forUserId, DateTimeOffset when) { try { var now = DateTimeOffset.Now; _db.Turns.Add(new Turn { ActivityId = activityId, CreatorId = byUserId, UserId = forUserId, Occurred = when }); var activity = GetActivity(activityId, false, true); if (activity == null) { _logger.LogError($"Activity {activityId} is missing"); return(Result.Failure <ActivityDetails, TurnError>(TurnError.ActivityMissing)); } if (_appSettings.Value.ValidateActivityModifiedDate && activity.ModifiedDate != activityModifiedDate) { _logger.LogWarning($"Activity {activity.Id} was modified {activity.ModifiedDate} and doesn't match {activityModifiedDate}"); return(Result.Failure <ActivityDetails, TurnError>(TurnError.ActivityModified)); } var details = ActivityDetails.Calculate(activity, byUserId, _mapper); var turnTaker = await _db.Users.FindAsync(forUserId); FormattableString fs = $"{turnTaker.DisplayName} took a turn."; var myTurnBuilder = new StringBuilder().AppendFormattable(fs); var otherTurnBuilder = new StringBuilder().AppendFormattable(fs); if (details.CurrentTurnUserId.HasValue) { otherTurnBuilder.AppendFormattable($" It's {details.CurrentTurnUserDisplayName}'s turn."); myTurnBuilder.AppendFormattable($" It's your turn."); } if (details.Due.HasValue) { fs = $" Due in {(details.Due.Value - now).ToDisplayString()}."; otherTurnBuilder.AppendFormattable(fs); myTurnBuilder.AppendFormattable(fs); } var myTurnMessage = myTurnBuilder.ToString(); var otherTurnMessage = otherTurnBuilder.ToString(); var url = $"{_appSettings.Value.PushNotifications.ServerUrl}/activity/{activityId}"; var failures = new List <PushFailure>(); foreach (var participant in activity.Participants) { var pushNotified = false; foreach (var setting in participant.NotificationSettings.OrderBy(x => x.Type)) { switch (setting.Type) { case NotificationType.OverdueAnybody: case NotificationType.OverdueMine: setting.NextCheck = now; // send a close push notification in case they still have a previous notification open but not if they // already got a notification about a turn being taken because that will replace any existing notification if (setting.Push && !pushNotified) { failures.AddRange(await _pushNotificationService.SendCloseToAllDevicesAsync("turn", setting.Participant.UserId, activityId.ToString())); pushNotified = true; } break; case NotificationType.TurnTakenAnybody: if (setting.Push) { failures.AddRange(await _pushNotificationService.SendToAllDevicesAsync("turn", setting.Participant.UserId, activity.Name, otherTurnMessage, url, activityId.ToString())); pushNotified = true; } break; case NotificationType.TurnTakenMine: if (details.CurrentTurnUserId.HasValue && setting.Push && setting.Participant.UserId == details.CurrentTurnUserId) { failures.AddRange(await _pushNotificationService.SendToAllDevicesAsync("turn", setting.Participant.UserId, activity.Name, myTurnMessage, url, activityId.ToString())); pushNotified = true; } break; default: _logger.LogError($"Unhandled notification type {setting.Type}"); break; } } } // Ensure we are done sending each push message before cleaning up failures and continuing await _pushNotificationService.CleanupFailuresAsync(failures); // Always mark the activity as modified when taking a turn so our checks elsewhere that compare // the modified timestamp will still work for activities that wouldn't normally have anything update, // like when they're non-periodic or the next-turn user doesn't change. _db.Entry(activity).State = EntityState.Modified; await _db.SaveChangesAsync(); details.Update(activity); return(Result.Success <ActivityDetails, TurnError>(details)); } catch (Exception e) { _logger.LogError(e, "Failed to take a turn"); return(Result.Failure <ActivityDetails, TurnError>(TurnError.Exception)); } }