📄 src/Api/TikTokApiEndpoints.cs
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Quartz;
using Slopper.Api.TikTokAuth;

namespace Slopper.Api;

public static class TikTokApiEndpoints
{
    extension(IEndpointRouteBuilder endpoints)
    {
        public IEndpointConventionBuilder MapTikTokApi()
        {
            var tiktok = endpoints.MapGroup("/tiktok").RequireTikTokAuthorization();

            tiktok.MapGet("/login", Login).WithDisplayName("TikTok Login");

            tiktok.MapGet("/upload", GetUploadJobStatus).WithDisplayName("Get TikTok upload job status.");
            tiktok.MapPut("/upload", StartUploadJob).WithDisplayName("Triggers a job uploading clips to TikTok.");

            return tiktok;
        }
    }

    private static RedirectHttpResult Login() => TypedResults.Redirect("/admin/tiktok");

    private static async Task<Ok<JobStatus>> GetUploadJobStatus(
        [FromServices] ISchedulerFactory schedulerFactory,
        CancellationToken cancellationToken
    )
    {
        var scheduler = await schedulerFactory.GetScheduler(cancellationToken);
        var isRunning = await IsUploadJobRunning(scheduler, cancellationToken);
        return TypedResults.Ok(new JobStatus(isRunning));
    }

    private static async Task<Accepted> StartUploadJob(
        [FromServices] TikTokAccessTokenProvider accessTokenProvider,
        HttpContext httpContext,
        [FromServices] ISchedulerFactory schedulerFactory,
        CancellationToken cancellationToken
    )
    {
        var scheduler = await schedulerFactory.GetScheduler(cancellationToken);

        if (await IsUploadJobRunning(scheduler, cancellationToken))
        {
            return TypedResults.Accepted("/api/tiktok/upload");
        }

        var result = await httpContext.AuthenticateAsync();
        var token =
            result.Properties?.GetTokenValue("access_token")
            ?? throw new Exception("TikTok access token is not available");

        accessTokenProvider.AccessToken = token;
        await scheduler.TriggerJob(UploadJob.Key, new JobDataMap { ["platform"] = "TikTok" }, cancellationToken);
        return TypedResults.Accepted("/api/tiktok/upload");
    }

    private static async Task<bool> IsUploadJobRunning(IScheduler scheduler, CancellationToken cancellationToken)
    {
        var currentlyExecutingJobs = await scheduler.GetCurrentlyExecutingJobs(cancellationToken);
        return currentlyExecutingJobs.Any(j =>
            j.JobDetail.Key == UploadJob.Key && j.MergedJobDataMap.GetString("platform") is "TikTok"
        );
    }
}