public async Task <IActionResult> GetAgeGroups(
            String name,
            CancellationToken cancellationToken)
        {
            await using var connection = this.dbConnectionFactory();
            await connection.OpenAsync(cancellationToken);

            await using var dbContext = new SegmentChallengeDbContext(connection);

            var challengeTable = dbContext.Set <Challenge>();
            var ageGroupsTable = dbContext.Set <AgeGroup>();

            var challenge = await challengeTable.SingleOrDefaultAsync(
                c => c.Name == name,
                cancellationToken
                );

            if (challenge == null)
            {
                return(NotFound());
            }

            return(new JsonResult(
                       await ageGroupsTable
                       .Where(ag => ag.ChallengeId == challenge.Id).ToListAsync(cancellationToken)
                       ));
        }
        private async Task RefreshAllChallengesAsync(CancellationToken cancellationToken)
        {
            logger.LogDebug("Refreshing all active challenges");
            await using var connection = this.dbConnectionFactory();
            await connection.OpenAsync(cancellationToken);

            await using var dbContext = new SegmentChallengeDbContext(connection);
            var updatesTable = dbContext.Set <Update>();

            var pendingUpdates = await
                                 updatesTable
                                 .Where(u => u.EndTime == null && u.AthleteId == null)
                                 .CountAsync(cancellationToken);

            if (pendingUpdates > 0)
            {
                // Ruh-roh
                this.logger.LogWarning(
                    "Unable to start auto refresh process because there is an unfinished update already running."
                    );
                return;
            }

            this.taskService.QueueTask <EffortRefresher>(
                (service, taskCancellationToken) => service.RefreshAllChallenges(taskCancellationToken)
                );
        }
Example #3
0
        public async Task <IActionResult> UpdateSelf(
            [FromBody] AthleteProfile profile,
            CancellationToken cancellationToken)
        {
            if (!(User is JwtCookiePrincipal identity))
            {
                return(Unauthorized());
            }

            await using var connection = this.dbConnectionFactory();
            await connection.OpenAsync(cancellationToken);

            await using var dbContext = new SegmentChallengeDbContext(connection);

            var athleteTable = dbContext.Set <Athlete>();

            var athlete =
                await athleteTable.FindAsync(new Object[] { identity.UserId }, cancellationToken);

            if (athlete == null)
            {
                // Odd
                return(NotFound());
            }
            else
            {
                athlete.BirthDate = profile.BirthDate;
                athlete.Gender    = profile.Gender;
                athlete.Email     = profile.Email;

                await dbContext.SaveChangesAsync(cancellationToken);

                Response.Cookies.Append(
                    "id_token",
                    StravaConnectController.CreateAthleteJwt(
                        this.challengeConfiguration.Value,
                        athlete
                        )
                    );

                return(new JsonResult(new AthleteProfile {
                    Username = athlete.Username,
                    FirstName = athlete.FirstName,
                    LastName = athlete.LastName,
                    BirthDate = athlete.BirthDate,
                    Gender = athlete.Gender,
                    Email = athlete.Email
                }));
            }
        }
        public async Task <IActionResult> Register(
            String name,
            CancellationToken cancellationToken)
        {
            if (!(User is JwtCookiePrincipal identity))
            {
                return(Unauthorized());
            }

            await using var connection = this.dbConnectionFactory();
            await connection.OpenAsync(cancellationToken);

            await using var dbContext = new SegmentChallengeDbContext(connection);

            var challengeTable    = dbContext.Set <Challenge>();
            var registrationTable = dbContext.Set <ChallengeRegistration>();

            var challenge = await challengeTable.SingleOrDefaultAsync(
                c => c.Name == name,
                cancellationToken
                );

            if (challenge == null)
            {
                return(NotFound());
            }

            var registration = await registrationTable.SingleOrDefaultAsync(
                r => r.ChallengeId == challenge.Id && r.AthleteId == identity.UserId,
                cancellationToken
                );

            if (registration == null)
            {
                await registrationTable.AddAsync(
                    new ChallengeRegistration {
                    ChallengeId = challenge.Id,
                    AthleteId   = identity.UserId
                },
                    cancellationToken
                    );

                await dbContext.SaveChangesAsync(cancellationToken);
            }

            return(new JsonResult(new {
                registered = true
            }));
        }
        public async Task RefreshEfforts(
            Int32 updateId,
            String challengeName,
            CancellationToken cancellationToken)
        {
            this.logger.LogDebug(
                "Refreshing all Efforts for Challenge {ChallengeName} (Update {UpdateId})",
                challengeName,
                updateId
                );

            try {
                await using var connection = this.dbConnectionFactory();
                await connection.OpenAsync(cancellationToken);

                await using var dbContext = new SegmentChallengeDbContext(connection);

                var challengeTable = dbContext.Set <Challenge>();
                var updatesTable   = dbContext.Set <Update>();

                var challenge = await challengeTable.SingleOrDefaultAsync(
                    c => c.Name == challengeName,
                    cancellationToken
                    );

                if (challenge == null)
                {
                    this.logger.LogError(
                        "Refresh Efforts Failed. Challenge not found: {ChallengeName}",
                        challengeName
                        );

                    return;
                }

                var update =
                    await updatesTable.FindAsync(new Object[] { updateId }, cancellationToken);

                await RefreshEffortsInternal(dbContext, challenge, update, cancellationToken);
            } catch (Exception ex) {
                this.logger.LogError(
                    "Refresh Efforts Failed. Unexpected Exception: {Message}",
                    1,
                    ex,
                    ex.Message
                    );
            }
        }
        public async Task <IActionResult> List(CancellationToken cancellationToken)
        {
            await using var connection = this.dbConnectionFactory();
            await connection.OpenAsync(cancellationToken);

            await using var dbContext = new SegmentChallengeDbContext(connection);

            var challengeTable = dbContext.Set <Challenge>();

            return(new JsonResult(
                       await
                       challengeTable
                       .OrderByDescending(c => c.StartDate)
                       .ToListAsync(cancellationToken: cancellationToken)
                       ));
        }
        public async Task RefreshAllChallenges(CancellationToken cancellationToken)
        {
            this.logger.LogDebug("RefreshAllChallenges Starting");
            await using var connection = this.dbConnectionFactory();
            await connection.OpenAsync(cancellationToken);

            await using var dbContext = new SegmentChallengeDbContext(connection);

            var challengeTable = dbContext.Set <Challenge>();
            var updatesTable   = dbContext.Set <Update>();

            var activeChallenges = await
                                   challengeTable
                                   .Where(c => c.EndDate > DateTime.UtcNow)
                                   .ToListAsync(cancellationToken);

            foreach (var challenge in activeChallenges)
            {
                var pendingUpdates = await
                                     updatesTable
                                     .Where(u => u.EndTime == null && u.AthleteId == null)
                                     .CountAsync(cancellationToken);

                if (pendingUpdates > 0)
                {
                    this.logger.LogWarning(
                        "Unable to continue auto refresh of challenge efforts because another update is currently running."
                        );
                    break;
                }

                var update = updatesTable.Add(new Update {
                    ChallengeId = challenge.Id
                });

                await dbContext.SaveChangesAsync(cancellationToken);

                logger.LogDebug("Refreshing all efforts for challenge {ChallengeId} (Update {UpdateId})", challenge.Id, update.Entity.Id);
                await this.RefreshEffortsInternal(dbContext, challenge, update.Entity, cancellationToken);
            }

            this.logger.LogDebug("RefreshAllChallenges Complete");
        }
        public async Task <IActionResult> AllAthletes(String name, CancellationToken cancellationToken)
        {
            await using var connection = this.dbConnectionFactory();
            await connection.OpenAsync(cancellationToken);

            await using var dbContext = new SegmentChallengeDbContext(connection);

            var challengeTable    = dbContext.Set <Challenge>();
            var athleteTable      = dbContext.Set <Athlete>();
            var registrationTable = dbContext.Set <ChallengeRegistration>();

            var challenge = await challengeTable.SingleOrDefaultAsync(
                c => c.Name == name,
                cancellationToken
                );

            if (challenge == null)
            {
                return(NotFound());
            }

            var athletes = await
                           athleteTable
                           .Join(
                registrationTable,
                a => a.Id,
                r => r.AthleteId,
                (a, r) => new { Athlete = a, Registration = r })
                           .Where(row => row.Registration.ChallengeId == challenge.Id)
                           .Select(row => row.Athlete)
                           .ToListAsync(cancellationToken);

            return(new JsonResult(
                       athletes
                       .Where(a => a.Gender.HasValue && a.BirthDate.HasValue)
                       .Select(a => new {
                id = a.Id,
                displayName = a.GetDisplayName(),
                gender = a.Gender.ToString(),
                age = challenge.StartDate.Year - a.BirthDate.Value.Year
            })
                       ));
        }
Example #9
0
        public async Task <IActionResult> GetSelf(CancellationToken cancellationToken)
        {
            if (!(User is JwtCookiePrincipal identity))
            {
                return(Unauthorized());
            }

            await using var connection = this.dbConnectionFactory();
            await connection.OpenAsync(cancellationToken);

            await using var dbContext = new SegmentChallengeDbContext(connection);

            var athleteTable = dbContext.Set <Athlete>();

            var athlete =
                await athleteTable.FindAsync(new Object[] { identity.UserId }, cancellationToken);

            if (athlete == null)
            {
                // Odd
                return(NotFound());
            }
            else
            {
                return(new JsonResult(new AthleteProfile {
                    Username = athlete.Username,
                    FirstName = athlete.FirstName,
                    LastName = athlete.LastName,
                    BirthDate = athlete.BirthDate,
                    Gender = athlete.Gender,
                    Email = athlete.Email,
                    IsAdmin = this.siteConfiguration.Value.Administrators != null &&
                              this.siteConfiguration.Value.Administrators.Contains(identity.UserId)
                }));
            }
        }
        public async Task RefreshAthleteEfforts(
            Int32 updateId,
            String challengeName,
            Int64 athleteId,
            CancellationToken cancellationToken)
        {
            this.logger.LogDebug(
                "Refreshing all Efforts for Challenge {ChallengeName} (Update {UpdateId})",
                challengeName,
                updateId
                );

            try {
                await using var connection = this.dbConnectionFactory();
                await connection.OpenAsync(cancellationToken);

                await using var dbContext = new SegmentChallengeDbContext(connection);

                var challengeTable     = dbContext.Set <Challenge>();
                var registrationsTable = dbContext.Set <ChallengeRegistration>();
                var athletesTable      = dbContext.Set <Athlete>();
                var updatesTable       = dbContext.Set <Update>();

                var challenge = await challengeTable.SingleOrDefaultAsync(
                    c => c.Name == challengeName,
                    cancellationToken
                    );

                if (challenge == null)
                {
                    this.logger.LogError(
                        "Refresh Efforts Failed. Challenge not found: {ChallengeName}",
                        challengeName
                        );

                    return;
                }

                var athlete = await
                              registrationsTable
                              .Join(
                    athletesTable,
                    cr => cr.AthleteId,
                    a => a.Id,
                    (cr, a) => new { Registration = cr, Athlete = a })
                              .Where(ra =>
                                     ra.Registration.ChallengeId == challenge.Id &&
                                     ra.Athlete.Id == athleteId)
                              .Where(ra => ra.Athlete.Gender != null && ra.Athlete.BirthDate != null)
                              .Select(ra => ra.Athlete)
                              .FirstOrDefaultAsync(cancellationToken);

                if (athlete == null)
                {
                    this.logger.LogError(
                        "Refresh Efforts Failed. Athlete not registered: {AthleteId} {ChallengeName}",
                        athleteId,
                        challengeName
                        );

                    return;
                }

                if (athlete.Id < 0)
                {
                    // This athlete is not a strava user. Manual upload required.
                    return;
                }

                var update =
                    await updatesTable.FindAsync(new Object[] { updateId }, cancellationToken);

                update.StartTime = DateTime.UtcNow;
                await dbContext.SaveChangesAsync(cancellationToken);

                var(activitiesUpdated, activitiesSkipped, effortsUpdated, error) =
                    await RefreshAthleteEffortsInternal(
                        dbContext,
                        update,
                        challenge,
                        athlete,
                        cancellationToken);

                update.ActivityCount        += activitiesUpdated;
                update.SkippedActivityCount += activitiesSkipped;
                update.EffortCount          += effortsUpdated;
                if (error)
                {
                    update.ErrorCount++;
                }

                update.AthleteCount = 1;
                update.Progress     = 1;
                update.EndTime      = DateTime.UtcNow;
                await dbContext.SaveChangesAsync(cancellationToken);
            } catch (Exception ex) {
                this.logger.LogError(
                    "Refresh Efforts Failed. Unexpected Exception: {Message}",
                    new EventId(2),
                    ex,
                    ex.Message
                    );

                try {
                    await using var connection = this.dbConnectionFactory();
                    await connection.OpenAsync(cancellationToken);

                    await using var dbContext = new SegmentChallengeDbContext(connection);

                    var updatesTable = dbContext.Set <Update>();
                    var update       = await updatesTable.SingleOrDefaultAsync(u => u.Id == updateId, cancellationToken);

                    if (update != null)
                    {
                        update.EndTime = DateTime.UtcNow;
                        await dbContext.SaveChangesAsync(cancellationToken);
                    }
                } catch (Exception ex2) {
                    this.logger.LogError(
                        "Unable to mark update {UpdateId} failed. Unexpected Exception: {Message}",
                        new EventId(2),
                        ex2,
                        updateId,
                        ex2.Message
                        );
                }
            }
        }
        RefreshAthleteEffortsInternal(
            SegmentChallengeDbContext dbContext,
            Update update,
            Challenge challenge,
            Athlete athlete,
            CancellationToken cancellationToken)
        {
            // var athletesTable = dbContext.Set<Athlete>();
            var effortsTable         = dbContext.Set <Effort>();
            var activityUpdatesTable = dbContext.Set <ActivityUpdate>();

            var stravaClient =
                new HttpClient {
                BaseAddress = new Uri("https://www.strava.com")
            };

            // If the athlete's token is expired, refresh it
            // In theory we should do this before each call, but we'll fudge it by assuming we can
            // handle a single athlete in fewer than 10 minutes.
            if (athlete.TokenExpiration <= DateTime.UtcNow.AddMinutes(-10))
            {
                var response =
                    await this.apiHelper.MakeThrottledApiRequest(
                        () => stravaClient.PostAsync(
                            "/api/v3/oauth/token",
                            new FormUrlEncodedContent(new Dictionary <string, string> {
                    { "client_id", this.stravaConfiguration.Value.ClientId },
                    { "client_secret", this.stravaConfiguration.Value.ClientSecret },
                    { "refresh_token", athlete.RefreshToken },
                    { "grant_type", "refresh_token" }
                }), cancellationToken),
                        cancellationToken
                        );

                if (response.IsSuccessStatusCode)
                {
                    var session =
                        await response.Content.ReadAsAsync <StravaSession>(cancellationToken);

                    athlete.AccessToken     = session.AccessToken;
                    athlete.RefreshToken    = session.RefreshToken;
                    athlete.TokenExpiration =
                        StravaApiHelper.DateTimeFromUnixTime(session.ExpiresAt);

                    await dbContext.SaveChangesAsync(cancellationToken);
                }
            }

            stravaClient.DefaultRequestHeaders.Authorization =
                new AuthenticationHeaderValue("Bearer", athlete.AccessToken);

            var previousUpdates = await
                                  activityUpdatesTable
                                  .Where(u => u.AthleteId == athlete.Id && u.ChallengeId == challenge.Id)
                                  .ToDictionaryAsync(u => u.ActivityId, cancellationToken);

            var activitiesUpdated = 0;
            var activitiesSkipped = 0;
            var effortsUpdated    = 0;

            // For each activity between start and end of challenge
            for (var pageNumber = 1; !cancellationToken.IsCancellationRequested; pageNumber++)
            {
                var response =
                    await this.apiHelper.MakeThrottledApiRequest(
                        () => stravaClient.GetAsync(
                            $"/api/v3/athlete/activities?after={challenge.StartDate.AddDays(-1).ToUnixTime()}&before={challenge.EndDate.ToUnixTime()}&page={pageNumber}&per_page=200",
                            cancellationToken),
                        cancellationToken
                        );

                if (response.IsSuccessStatusCode)
                {
                    var activities =
                        await response.Content.ReadAsAsync <StravaActivity[]>(cancellationToken);

                    response.Dispose();
                    response = null;

                    if (activities == null || activities.Length == 0)
                    {
                        break;
                    }

                    foreach (var activity in activities)
                    {
                        if (activity.Type == "Ride")
                        {
                            if (activity.Flagged)
                            {
                                this.logger.LogInformation(
                                    "Skipping Activity {ActivityId} for Athlete {AthleteId} because it has been flagged.",
                                    activity.Id,
                                    athlete.Id
                                    );
                            }
                            else if (previousUpdates.ContainsKey(activity.Id))
                            {
                                activitiesSkipped++;
                            }
                            else
                            {
                                var activityDetailsResponse =
                                    await apiHelper.MakeThrottledApiRequest(
                                        () => stravaClient.GetAsync(
                                            $"/api/v3/activities/{activity.Id}?include_all_efforts=true",
                                            cancellationToken),
                                        cancellationToken
                                        );

                                if (activityDetailsResponse.IsSuccessStatusCode)
                                {
                                    var activityDetails =
                                        await activityDetailsResponse.Content
                                        .ReadAsAsync <StravaActivityDetails>(cancellationToken);

                                    var relevantEfforts =
                                        activityDetails.SegmentEfforts
                                        .Where(e => e.Segment.Id == challenge.SegmentId)
                                        .Where(e =>
                                               e.StartDate >= challenge.StartDate &&
                                               e.StartDate.AddSeconds(e.ElapsedTime) <= challenge.EndDate)
                                        .ToList();

                                    if (relevantEfforts.Count > 0)
                                    {
                                        // Save Efforts
                                        foreach (var effort in relevantEfforts)
                                        {
                                            var existingEffort =
                                                await effortsTable.SingleOrDefaultAsync(e => e.Id == effort.Id, cancellationToken : cancellationToken);

                                            if (existingEffort != null)
                                            {
                                                existingEffort.StartDate   = effort.StartDate;
                                                existingEffort.ElapsedTime = challenge.UseMovingTime ? effort.MovingTime : effort.ElapsedTime;
                                            }
                                            else
                                            {
                                                await effortsTable.AddAsync(
                                                    new Effort {
                                                    Id          = effort.Id,
                                                    AthleteId   = athlete.Id,
                                                    ActivityId  = activity.Id,
                                                    SegmentId   = challenge.SegmentId,
                                                    ElapsedTime = challenge.UseMovingTime ? effort.MovingTime : effort.ElapsedTime,
                                                    StartDate   = effort.StartDate
                                                },
                                                    cancellationToken
                                                    );
                                            }
                                        }
                                    }

                                    await activityUpdatesTable.AddAsync(
                                        new ActivityUpdate {
                                        ChallengeId = challenge.Id,
                                        ActivityId  = activity.Id,
                                        AthleteId   = athlete.Id,
                                        UpdateId    = update.Id,
                                        UpdatedAt   = DateTime.UtcNow
                                    },
                                        cancellationToken
                                        );

                                    await dbContext.SaveChangesAsync(cancellationToken);

                                    activitiesUpdated++;
                                    effortsUpdated += relevantEfforts.Count;
                                }
                                else
                                {
                                    logger.LogError(
                                        "An HTTP error occurred attempting to fetch activity details for Athlete {AthleteId} Activity {ActivityId} - Status {StatusCode}: {Content}",
                                        athlete.Id,
                                        activity.Id,
                                        activityDetailsResponse.StatusCode,
                                        await activityDetailsResponse.Content.ReadAsStringAsync()
                                        );

                                    // Give up
                                    return(activitiesUpdated, activitiesSkipped, effortsUpdated, error : true);
                                }
                            }
                        }
                    }
                }
                else
                {
                    logger.LogError(
                        "An HTTP error occurred attempting to fetch activities for Athlete {AthleteId} - Status {StatusCode}: {Content}",
                        athlete.Id,
                        response.StatusCode,
                        await response.Content.ReadAsStringAsync()
                        );

                    // Give up
                    return(activitiesUpdated, activitiesSkipped, effortsUpdated, true);
                }
            }

            return(activitiesUpdated, activitiesSkipped, effortsUpdated, false);
        }
        private async Task RefreshEffortsInternal(
            SegmentChallengeDbContext dbContext,
            Challenge challenge,
            Update update,
            CancellationToken cancellationToken)
        {
            var registrationsTable = dbContext.Set <ChallengeRegistration>();
            var athletesTable      = dbContext.Set <Athlete>();

            var athletes = await
                           registrationsTable
                           .Join(
                athletesTable,
                cr => cr.AthleteId,
                a => a.Id,
                (cr, a) => new { Registration = cr, Athlete = a })
                           .Where(ra => ra.Registration.ChallengeId == challenge.Id)
                           .Where(ra => ra.Athlete.Gender != null && ra.Athlete.BirthDate != null)
                           .Select(ra => ra.Athlete)
                           .ToListAsync(cancellationToken);

            update.StartTime = DateTime.UtcNow;
            await dbContext.SaveChangesAsync(cancellationToken);

            foreach (var athlete in athletes)
            {
                this.logger.LogDebug(
                    "Updating {ChallengeName} efforts for athlete {AthleteId}.",
                    challenge.Name,
                    athlete.Id
                    );

                // Update efforts
                try {
                    var(activitiesUpdated, activitiesSkipped, effortsUpdated, error) =
                        await RefreshAthleteEffortsInternal(
                            dbContext,
                            update,
                            challenge,
                            athlete,
                            cancellationToken);

                    update.ActivityCount        += activitiesUpdated;
                    update.SkippedActivityCount += activitiesSkipped;
                    update.EffortCount          += effortsUpdated;
                    if (error)
                    {
                        update.ErrorCount++;
                    }
                } catch (TaskCanceledException) {
                    throw;
                } catch (OperationCanceledException) {
                    throw;
                } catch (Exception ex) {
                    update.ErrorCount++;
                    this.logger.LogError(
                        "An unexpected exception occurred while refreshing efforts for Athlete {AthleteId} (Challenge: {ChallengeId}",
                        2,
                        ex,
                        athlete.Id,
                        challenge.Id
                        );
                }

                update.AthleteCount++;

                update.Progress =
                    (Single)update.AthleteCount / athletes.Count;

                await dbContext.SaveChangesAsync(cancellationToken);
            }

            update.EndTime = DateTime.UtcNow;
            await dbContext.SaveChangesAsync(cancellationToken);
        }
        public async Task <IActionResult> GetEfforts(
            String name,
            CancellationToken cancellationToken)
        {
            await using var connection = this.dbConnectionFactory();
            await connection.OpenAsync(cancellationToken);

            await using var dbContext = new SegmentChallengeDbContext(connection);

            var challengeTable    = dbContext.Set <Challenge>();
            var registrationTable = dbContext.Set <ChallengeRegistration>();
            var ageGroupsTable    = dbContext.Set <AgeGroup>();
            var effortsTable      = dbContext.Set <Effort>();
            var athleteTable      = dbContext.Set <Athlete>();

            var challenge = await challengeTable.SingleOrDefaultAsync(
                c => c.Name == name,
                cancellationToken
                );

            if (challenge == null)
            {
                return(NotFound());
            }

            var ageGroups = await
                            ageGroupsTable
                            .Where(ag => ag.ChallengeId == challenge.Id)
                            .OrderBy(ag => ag.MaximumAge)
                            .ToListAsync(cancellationToken);

            var efforts = await
                          effortsTable
                          .Where(e =>
                                 e.SegmentId == challenge.SegmentId &&
                                 e.StartDate >= challenge.StartDate &&
                                 e.StartDate <= challenge.EndDate)
                          .Join(
                athleteTable,
                e => e.AthleteId,
                a => a.Id,
                (e, a) => new {
                Effort  = e,
                Athlete = a
            })
                          .Join(
                registrationTable,
                row => row.Athlete.Id,
                r => r.AthleteId,
                (row, reg) =>
                new {
                Effort       = row.Effort,
                Athlete      = row.Athlete,
                Registration = reg
            })
                          .Where(row => row.Registration.ChallengeId == challenge.Id)
                          .OrderBy(row => row.Athlete.Id)
                          .ToListAsync(cancellationToken: cancellationToken);

            var results = new List <(Effort Effort, Athlete Athlete, Int32 LapCount)>();

            if (challenge.Type == ChallengeType.MostLaps)
            {
                foreach (var effortGroup in efforts.GroupBy(e => e.Athlete.Id))
                {
                    var(effort, athlete, lapCount) =
                        effortGroup.Aggregate(
                            (effort: (Effort)null, athlete: (Athlete)null, lapCount: 0),
                            (total, nextEffort) => {
                        if (total.effort == null)
                        {
                            return(nextEffort.Effort, nextEffort.Athlete, 1);
                        }
                        else
                        {
                            return(total.effort.WithElapsedTime(total.effort.ElapsedTime + nextEffort.Effort.ElapsedTime),
                                   total.athlete,
                                   total.lapCount + 1);
                        }
                    });
                    results.Add((effort, effortGroup.First().Athlete, lapCount));
                }
            }
            else
            {
                Athlete currentAthlete = null;
                Effort  bestEffort     = null;

                foreach (var effort in efforts.Append(null))
                {
                    if (effort == null || effort.Athlete.Id != currentAthlete?.Id)
                    {
                        if (bestEffort != null)
                        {
                            results.Add((bestEffort, currentAthlete, 1));
                        }

                        if (effort != null)
                        {
                            currentAthlete = effort.Athlete;
                            bestEffort     = effort.Effort;
                        }
                    }
                    else if (bestEffort == null ||
                             effort.Effort.ElapsedTime < bestEffort.ElapsedTime)
                    {
                        bestEffort = effort.Effort;
                    }
                }
            }

            (String Gender, Int32 MaxAge) GetCategory(Athlete athlete)
            {
                var birthDateYear =
                    (athlete.BirthDate?.Year).GetValueOrDefault(DateTime.UtcNow.Year - 90);
                var age = DateTime.UtcNow.Year - birthDateYear;

                var ageGroup = ageGroups.SkipWhile(ag => age > ag.MaximumAge).First();

                return(athlete.Gender.GetValueOrDefault('M').ToString(), ageGroup.MaximumAge);
            }

            var resultsByCategory = new List <(Effort Effort, Athlete Athlete, Int32 LapCount, Boolean IsKOM)>();

            (String Gender, Int32 MaxAge)currentCategory = (null, 0);

            foreach (var(effort, athlete, lapCount) in results.OrderBy(e => GetCategory(e.Athlete)).ThenByDescending(e => e.LapCount)
                     .ThenBy(e => e.Effort.ElapsedTime))
            {
                var category = GetCategory(athlete);
                if (category != currentCategory)
                {
                    resultsByCategory.Add((effort, athlete, lapCount, true));
                    currentCategory = category;
                }
                else if (resultsByCategory.Count > 0 &&
                         resultsByCategory[^ 1].LapCount == lapCount &&
                         resultsByCategory[^ 1].Effort.ElapsedTime == effort.ElapsedTime)
                {
                    // Tie for first
                    resultsByCategory.Add((effort, athlete, lapCount, true));
                }
                else
                {
                    resultsByCategory.Add((effort, athlete, lapCount, false));
                }
            }

            return(new JsonResult(
                       resultsByCategory
                       .OrderByDescending(e => e.LapCount)
                       .ThenBy(e => e.Effort.ElapsedTime)
                       .ThenBy(e => e.Effort.StartDate)
                       .Select(e =>
                               new {
                id = e.Effort.Id,
                athleteId = e.Athlete.Id,
                athleteName = e.Athlete.GetDisplayName(),
                athleteGender = e.Athlete.Gender,
                athleteAge = e.Athlete.BirthDate?.ToRacingAge(challenge.StartDate),
                activityId = e.Effort.ActivityId,
                lapCount = e.LapCount,
                elapsedTime = e.Effort.ElapsedTime,
                startDate = e.Effort.StartDate,
                isKOM = e.IsKOM
            })
                       ));
        }
        public async Task <IActionResult> Authorize(
            [FromQuery] String state,
            [FromQuery] String code,
            [FromQuery] String scope,
            CancellationToken cancellationToken)
        {
            var expected_state = Request.Cookies["authentication_state"];

            if (!String.Equals(state, expected_state))
            {
                this.logger.LogWarning(
                    "The state {ActualState} did not match the expected authentication state {ExpectedState}",
                    state,
                    expected_state
                    );
            }

            var codeExchangeClient =
                new HttpClient {
                BaseAddress = new Uri("https://www.strava.com")
            };

            codeExchangeClient.DefaultRequestHeaders.Add("Accept", "application/json");

            var response =
                await this.apiHelper.MakeThrottledApiRequest(
                    () => codeExchangeClient.PostAsync(
                        "/api/v3/oauth/token",
                        new FormUrlEncodedContent(new Dictionary <string, string> {
                { "client_id", this.stravaConfiguration.Value.ClientId },
                { "client_secret", this.stravaConfiguration.Value.ClientSecret },
                { "code", code },
                { "grant_type", "authorization_code" }
            }),
                        cancellationToken
                        ),
                    cancellationToken);

            if (response.IsSuccessStatusCode)
            {
                var session =
                    await response.Content.ReadAsAsync <StravaSession>(cancellationToken);

                await using var connection = this.dbConnectionFactory();
                await connection.OpenAsync(cancellationToken);

                await using var dbContext = new SegmentChallengeDbContext(connection);

                var athleteTable = dbContext.Set <Athlete>();
                // Does user exist? If not create them.
                var existingAthlete =
                    await athleteTable.SingleOrDefaultAsync(a => a.Id == session.Athlete.Id,
                                                            cancellationToken);

                EntityEntry <Athlete> newAthlete = null;
                if (existingAthlete == null)
                {
                    newAthlete = await athleteTable.AddAsync(
                        new Athlete {
                        Id        = session.Athlete.Id,
                        Username  = session.Athlete.Username,
                        FirstName = session.Athlete.FirstName,
                        LastName  = session.Athlete.LastName,
                        Gender    = !String.IsNullOrEmpty(session.Athlete.Sex) ?
                                    session.Athlete.Sex[0] :
                                    (Char?)null,
                        ProfilePicture =
                            session.Athlete.ProfileMedium ?? session.Athlete.Profile,
                        AccessToken     = session.AccessToken,
                        RefreshToken    = session.RefreshToken,
                        TokenExpiration =
                            StravaApiHelper.DateTimeFromUnixTime(session.ExpiresAt)
                    },
                        cancellationToken
                        );
                }
                else
                {
                    existingAthlete.Username  = session.Athlete.Username;
                    existingAthlete.FirstName = session.Athlete.FirstName;
                    existingAthlete.LastName  = session.Athlete.LastName;
                    if (!String.IsNullOrEmpty(session.Athlete.Sex))
                    {
                        existingAthlete.Gender = session.Athlete.Sex[0];
                    }

                    existingAthlete.ProfilePicture =
                        session.Athlete.ProfileMedium ?? session.Athlete.Profile;
                    existingAthlete.AccessToken     = session.AccessToken;
                    existingAthlete.RefreshToken    = session.RefreshToken;
                    existingAthlete.TokenExpiration =
                        StravaApiHelper.DateTimeFromUnixTime(session.ExpiresAt);

                    athleteTable.Update(existingAthlete);
                }

                var changes = await dbContext.SaveChangesAsync(cancellationToken);

                if (changes != 1)
                {
                    logger.LogWarning(
                        $"Unexpected number of rows changed {(existingAthlete == null ? "creating" : "updating")} Athlete {{AthleteId}} ({{RowsChanged}})",
                        session.Athlete.Id,
                        changes
                        );
                }

                Response.Cookies.Append(
                    "id_token",
                    CreateAthleteJwt(
                        this.challengeConfiguration.Value,
                        existingAthlete ?? newAthlete?.Entity),
                    new CookieOptions {
                    Expires = DateTime.UtcNow.AddDays(this.challengeConfiguration.Value.TokenExpiration)
                }
                    );


                return(Redirect("/"));
            }
            else
            {
                logger.LogError(
                    "Authentication Failed with HTTP Status {StatusCode}: {Content}",
                    response.StatusCode,
                    await response.Content.ReadAsStringAsync()
                    );

                return(this.Problem(
                           $"An unexpected error occurred. Please contact {this.challengeConfiguration.Value.SupportContact}"));
            }
        }