feat(calendar): add application date integrations

This commit is contained in:
cesnimda
2026-08-31 22:24:38 +02:00
parent caf486dceb
commit 1152e05687
10 changed files with 547 additions and 2 deletions
@@ -21,6 +21,7 @@ public interface IMicrosoftGraphOAuthService
Task<IReadOnlyList<MicrosoftGraphMessageSummary>> ListThreadMessagesAsync(string ownerUserId, string conversationId, CancellationToken cancellationToken);
Task<MicrosoftGraphMessageDetail> GetMessageAsync(string ownerUserId, string messageId, CancellationToken cancellationToken);
Task<MicrosoftGraphSendResult> SendAsync(string ownerUserId, MicrosoftGraphSendRequest request, CancellationToken cancellationToken);
Task<ExternalCalendarEventResult> CreateCalendarEventAsync(string ownerUserId, ExternalCalendarEventRequest request, CancellationToken cancellationToken);
}
public sealed record MicrosoftGraphOAuthExchangeResult(string MailAddress);
@@ -47,7 +48,8 @@ internal sealed class MicrosoftGraphTokenResponse
public sealed class MicrosoftGraphOAuthService : IMicrosoftGraphOAuthService
{
public const string SendScope = "https://graph.microsoft.com/Mail.Send";
private const string Scope = $"openid email profile offline_access https://graph.microsoft.com/Mail.Read {SendScope}";
public const string CalendarScope = "https://graph.microsoft.com/Calendars.ReadWrite";
private const string Scope = $"openid email profile offline_access https://graph.microsoft.com/Mail.Read {SendScope} {CalendarScope}";
private readonly IConfiguration _cfg;
private readonly JobTrackerContext _db;
private readonly IDataProtector _protector;
@@ -340,6 +342,47 @@ public sealed class MicrosoftGraphOAuthService : IMicrosoftGraphOAuthService
string.Equals(value, "Mail.Send", StringComparison.OrdinalIgnoreCase));
}
public async Task<ExternalCalendarEventResult> CreateCalendarEventAsync(string ownerUserId, ExternalCalendarEventRequest request, CancellationToken cancellationToken)
{
var connection = await _db.MicrosoftGraphConnections.AsNoTracking().FirstOrDefaultAsync(item => item.OwnerUserId == ownerUserId, cancellationToken);
if (connection is null || !HasCalendarScope(connection.Scope))
throw new InvalidOperationException("Reconnect Outlook and approve calendar access before adding events.");
var startsAt = request.AllDay ? request.StartsAtUtc.UtcDateTime.Date : request.StartsAtUtc.UtcDateTime;
var endsAt = request.AllDay ? request.EndsAtUtc.UtcDateTime.Date : request.EndsAtUtc.UtcDateTime;
var payload = JsonSerializer.Serialize(new
{
subject = request.Summary,
body = new { contentType = "Text", content = request.Description ?? string.Empty },
location = new { displayName = request.Location ?? string.Empty },
start = new { dateTime = startsAt.ToString("yyyy-MM-ddTHH:mm:ss"), timeZone = "UTC" },
end = new { dateTime = endsAt.ToString("yyyy-MM-ddTHH:mm:ss"), timeZone = "UTC" },
isAllDay = request.AllDay,
transactionId = request.StableId
});
var accessToken = await GetValidAccessTokenAsync(ownerUserId, cancellationToken);
var client = _httpClientFactory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
using var response = await client.PostAsync(
"https://graph.microsoft.com/v1.0/me/events",
new StringContent(payload, System.Text.Encoding.UTF8, "application/json"),
cancellationToken);
response.EnsureSuccessStatusCode();
using var document = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(cancellationToken), cancellationToken: cancellationToken);
return new ExternalCalendarEventResult(
document.RootElement.TryGetProperty("id", out var id) ? id.GetString() : null,
document.RootElement.TryGetProperty("webLink", out var link) ? link.GetString() : null);
}
public static bool HasCalendarScope(string? scope)
{
if (string.IsNullOrWhiteSpace(scope)) return false;
return scope.Split(' ', StringSplitOptions.RemoveEmptyEntries).Any(value =>
string.Equals(value, CalendarScope, StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "Calendars.ReadWrite", StringComparison.OrdinalIgnoreCase));
}
private static void ValidateSendRequest(string to, string subject, string bodyText)
{
if (string.IsNullOrWhiteSpace(to)) throw new ArgumentException("Recipient is required.", nameof(to));