Files
social-media/backend/src/Socialize.Api/Modules/CalendarIntegrations/Handlers/UserCalendarExportFeed.cs
Jonathan Bourdon b66c10b681
Some checks failed
Backend CI/CD / build_and_deploy (push) Has been cancelled
Frontend CI/CD / build_and_deploy (push) Has been cancelled
Add calendar integrations and collaboration updates
2026-05-05 15:25:53 -04:00

225 lines
7.3 KiB
C#

using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using Socialize.Api.Data;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.CalendarIntegrations.Data;
using Socialize.Api.Modules.CalendarIntegrations.Services;
using Socialize.Api.Modules.Identity.Data;
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
public record UserCalendarExportFeedDto(
bool IsEnabled,
string? FeedUrl,
DateTimeOffset? CreatedAt,
DateTimeOffset? UpdatedAt,
DateTimeOffset? RevokedAt);
public class GetUserCalendarExportFeedHandler(AppDbContext dbContext)
: EndpointWithoutRequest<UserCalendarExportFeedDto>
{
public override void Configure()
{
Get("/api/calendar-integrations/export-feed");
Options(o => o.WithTags("Calendar Integrations"));
}
public override async Task HandleAsync(CancellationToken ct)
{
UserCalendarExportFeed? feed = await dbContext.UserCalendarExportFeeds
.SingleOrDefaultAsync(candidate => candidate.UserId == User.GetUserId(), ct);
await SendAsync(UserCalendarExportFeedMapper.ToDto(feed, UserCalendarExportFeedMapper.BuildFeedUrl(feed)), cancellation: ct);
}
}
public class EnableUserCalendarExportFeedHandler(AppDbContext dbContext)
: EndpointWithoutRequest<UserCalendarExportFeedDto>
{
public override void Configure()
{
Post("/api/calendar-integrations/export-feed/enable");
Options(o => o.WithTags("Calendar Integrations"));
}
public override async Task HandleAsync(CancellationToken ct)
{
Guid userId = User.GetUserId();
string token = CalendarExportFeedTokenService.GenerateToken();
string tokenHash = CalendarExportFeedTokenService.HashToken(token);
DateTimeOffset now = DateTimeOffset.UtcNow;
UserCalendarExportFeed? feed = await dbContext.UserCalendarExportFeeds
.SingleOrDefaultAsync(candidate => candidate.UserId == userId, ct);
if (feed is null)
{
feed = new UserCalendarExportFeed
{
Id = Guid.NewGuid(),
UserId = userId,
Token = token,
TokenHash = tokenHash,
UpdatedAt = now,
};
dbContext.UserCalendarExportFeeds.Add(feed);
}
else if (feed.TokenHash is null || feed.RevokedAt.HasValue)
{
feed.Token = token;
feed.TokenHash = tokenHash;
feed.RevokedAt = null;
feed.UpdatedAt = now;
}
else
{
token = string.Empty;
}
await dbContext.SaveChangesAsync(ct);
await SendAsync(UserCalendarExportFeedMapper.ToDto(feed, UserCalendarExportFeedMapper.BuildFeedUrl(feed, token)), cancellation: ct);
}
}
public class RegenerateUserCalendarExportFeedHandler(AppDbContext dbContext)
: EndpointWithoutRequest<UserCalendarExportFeedDto>
{
public override void Configure()
{
Post("/api/calendar-integrations/export-feed/regenerate");
Options(o => o.WithTags("Calendar Integrations"));
}
public override async Task HandleAsync(CancellationToken ct)
{
Guid userId = User.GetUserId();
string token = CalendarExportFeedTokenService.GenerateToken();
DateTimeOffset now = DateTimeOffset.UtcNow;
UserCalendarExportFeed? feed = await dbContext.UserCalendarExportFeeds
.SingleOrDefaultAsync(candidate => candidate.UserId == userId, ct);
if (feed is null)
{
feed = new UserCalendarExportFeed
{
Id = Guid.NewGuid(),
UserId = userId,
UpdatedAt = now,
};
dbContext.UserCalendarExportFeeds.Add(feed);
}
feed.TokenHash = CalendarExportFeedTokenService.HashToken(token);
feed.Token = token;
feed.RevokedAt = null;
feed.UpdatedAt = now;
await dbContext.SaveChangesAsync(ct);
await SendAsync(UserCalendarExportFeedMapper.ToDto(feed, UserCalendarExportFeedMapper.BuildFeedUrl(feed, token)), cancellation: ct);
}
}
public class RevokeUserCalendarExportFeedHandler(AppDbContext dbContext)
: EndpointWithoutRequest<UserCalendarExportFeedDto>
{
public override void Configure()
{
Delete("/api/calendar-integrations/export-feed");
Options(o => o.WithTags("Calendar Integrations"));
}
public override async Task HandleAsync(CancellationToken ct)
{
UserCalendarExportFeed? feed = await dbContext.UserCalendarExportFeeds
.SingleOrDefaultAsync(candidate => candidate.UserId == User.GetUserId(), ct);
if (feed is not null)
{
feed.TokenHash = null;
feed.Token = null;
feed.RevokedAt = DateTimeOffset.UtcNow;
feed.UpdatedAt = feed.RevokedAt.Value;
await dbContext.SaveChangesAsync(ct);
}
await SendAsync(UserCalendarExportFeedMapper.ToDto(feed, null), cancellation: ct);
}
}
public class GetUserCalendarExportFeedIcsHandler(
AppDbContext dbContext,
CalendarExportFeedService feedService)
: EndpointWithoutRequest
{
public override void Configure()
{
AllowAnonymous();
Get("/api/calendar-integrations/export-feed/{token}.ics");
Options(o => o.WithTags("Calendar Integrations"));
}
public override async Task HandleAsync(CancellationToken ct)
{
string? token = Route<string?>("token");
if (string.IsNullOrWhiteSpace(token))
{
await SendNotFoundAsync(ct);
return;
}
string tokenHash = CalendarExportFeedTokenService.HashToken(token);
UserCalendarExportFeed? feed = await dbContext.UserCalendarExportFeeds
.SingleOrDefaultAsync(candidate =>
candidate.TokenHash == tokenHash &&
!candidate.RevokedAt.HasValue,
ct);
if (feed is null)
{
await SendNotFoundAsync(ct);
return;
}
User? user = await dbContext.Users.SingleOrDefaultAsync(candidate => candidate.Id == feed.UserId, ct);
if (user is null)
{
await SendNotFoundAsync(ct);
return;
}
string appBaseUrl = $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}";
string ics = await feedService.BuildUserFeedAsync(feed.UserId, user.Email, appBaseUrl, ct);
HttpContext.Response.ContentType = "text/calendar; charset=utf-8";
await HttpContext.Response.WriteAsync(ics, ct);
}
}
file static class UserCalendarExportFeedMapper
{
public static UserCalendarExportFeedDto ToDto(UserCalendarExportFeed? feed, string? feedUrl)
{
return new UserCalendarExportFeedDto(
feed?.TokenHash is not null && !feed.RevokedAt.HasValue,
feedUrl,
feed?.CreatedAt,
feed?.UpdatedAt,
feed?.RevokedAt);
}
public static string? BuildFeedUrl(UserCalendarExportFeed? feed, string? token = null)
{
if (feed?.TokenHash is null || feed.RevokedAt.HasValue)
{
return null;
}
string effectiveToken = string.IsNullOrWhiteSpace(token) ? feed.Token ?? string.Empty : token;
return string.IsNullOrWhiteSpace(effectiveToken)
? null
: $"/api/calendar-integrations/export-feed/{effectiveToken}.ics";
}
}