Example #1
0
        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"));
            }
        }
Example #3
0
        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");
            }
        }
Example #4
0
        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));
            }
        }
Example #6
0
        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"));
            }
        }
Example #8
0
        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));
            }
        }