chore: init project
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
using InboxIntel.Application.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace InboxIntel.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// AI endpoints are read-only / advisory. They never trigger destructive
|
||||
/// actions - suggestions are returned for the user to act on via /cleanup.
|
||||
/// </summary>
|
||||
public class AiController : ApiControllerBase
|
||||
{
|
||||
private readonly IAiService _ai;
|
||||
public AiController(IAiService ai) => _ai = ai;
|
||||
|
||||
[HttpGet("status")]
|
||||
public IActionResult Status() => Ok(new { enabled = _ai.IsEnabled });
|
||||
|
||||
[HttpPost("classify/{emailId:guid}")]
|
||||
public async Task<IActionResult> Classify(Guid emailId, CancellationToken ct)
|
||||
=> Ok(await _ai.ClassifyAsync(UserId, emailId, ct));
|
||||
|
||||
[HttpGet("summary")]
|
||||
public async Task<IActionResult> Summary(CancellationToken ct)
|
||||
=> Ok(await _ai.SummarizeInboxAsync(UserId, ct));
|
||||
|
||||
[HttpGet("suggestions")]
|
||||
public async Task<IActionResult> Suggestions(CancellationToken ct)
|
||||
=> Ok(await _ai.SuggestCleanupAsync(UserId, ct));
|
||||
|
||||
[HttpPost("generate-query")]
|
||||
public async Task<IActionResult> GenerateQuery([FromBody] string naturalLanguage, CancellationToken ct)
|
||||
=> Ok(await _ai.GenerateQueryAsync(UserId, naturalLanguage, ct));
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using InboxIntel.Application.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace InboxIntel.Api.Controllers;
|
||||
|
||||
public class AnalyticsController : ApiControllerBase
|
||||
{
|
||||
private readonly IAnalyticsService _analytics;
|
||||
public AnalyticsController(IAnalyticsService analytics) => _analytics = analytics;
|
||||
|
||||
[HttpGet("dashboard")]
|
||||
public async Task<IActionResult> Dashboard(CancellationToken ct) => Ok(await _analytics.GetDashboardAsync(UserId, ct));
|
||||
|
||||
[HttpGet("health")]
|
||||
public async Task<IActionResult> Health(CancellationToken ct) => Ok(await _analytics.GetInboxHealthAsync(UserId, ct));
|
||||
|
||||
[HttpGet("top-senders")]
|
||||
public async Task<IActionResult> TopSenders([FromQuery] int take = 20, CancellationToken ct = default)
|
||||
=> Ok(await _analytics.GetTopSendersAsync(UserId, take, ct));
|
||||
|
||||
[HttpGet("volume")]
|
||||
public async Task<IActionResult> Volume([FromQuery] int days = 90, CancellationToken ct = default)
|
||||
=> Ok(await _analytics.GetVolumeOverTimeAsync(UserId, days, ct));
|
||||
|
||||
[HttpGet("heatmap")]
|
||||
public async Task<IActionResult> Heatmap(CancellationToken ct) => Ok(await _analytics.GetHeatmapAsync(UserId, ct));
|
||||
|
||||
[HttpGet("attachments")]
|
||||
public async Task<IActionResult> Attachments(CancellationToken ct) => Ok(await _analytics.GetAttachmentBreakdownAsync(UserId, ct));
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Asp.Versioning;
|
||||
using InboxIntel.Application.Abstractions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace InboxIntel.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
public abstract class ApiControllerBase : ControllerBase
|
||||
{
|
||||
private ICurrentUser? _currentUser;
|
||||
protected ICurrentUser CurrentUser => _currentUser ??= HttpContext.RequestServices.GetRequiredService<ICurrentUser>();
|
||||
protected Guid UserId => CurrentUser.UserId;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Security.Claims;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.Google;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace InboxIntel.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
/// <summary>Begins the Google OAuth2 login flow.</summary>
|
||||
[HttpGet("login")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Login([FromQuery] string? returnUrl = "/")
|
||||
=> Challenge(new AuthenticationProperties { RedirectUri = returnUrl }, GoogleDefaults.AuthenticationScheme);
|
||||
|
||||
[HttpPost("logout")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>Returns the currently signed-in user, or 401.</summary>
|
||||
[HttpGet("me")]
|
||||
[Authorize]
|
||||
public IActionResult Me() => Ok(new
|
||||
{
|
||||
UserId = User.FindFirstValue("inboxintel:uid"),
|
||||
Email = User.FindFirstValue(ClaimTypes.Email),
|
||||
Name = User.FindFirstValue(ClaimTypes.Name)
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using InboxIntel.Application.Abstractions;
|
||||
using InboxIntel.Application.DTOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace InboxIntel.Api.Controllers;
|
||||
|
||||
public class CleanupController : ApiControllerBase
|
||||
{
|
||||
private readonly ICleanupService _cleanup;
|
||||
public CleanupController(ICleanupService cleanup) => _cleanup = cleanup;
|
||||
|
||||
/// <summary>Preview which emails a cleanup action would affect. Always call before execute.</summary>
|
||||
[HttpPost("preview")]
|
||||
public async Task<IActionResult> Preview([FromBody] CleanupRequestDto request, CancellationToken ct)
|
||||
=> Ok(await _cleanup.PreviewAsync(UserId, request, ct));
|
||||
|
||||
/// <summary>Execute a cleanup action. Destructive actions require Confirmed = true.</summary>
|
||||
[HttpPost("execute")]
|
||||
public async Task<IActionResult> Execute([FromBody] CleanupRequestDto request, CancellationToken ct)
|
||||
{
|
||||
var result = await _cleanup.ExecuteAsync(UserId, request, ct);
|
||||
return result.Succeeded ? Ok(result.Value) : BadRequest(new { error = result.Error });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using InboxIntel.Application.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace InboxIntel.Api.Controllers;
|
||||
|
||||
public class ExportController : ApiControllerBase
|
||||
{
|
||||
private readonly IExportService _export;
|
||||
public ExportController(IExportService export) => _export = export;
|
||||
|
||||
/// <summary>Export an inbox report. format = pdf | csv | json.</summary>
|
||||
[HttpGet("report")]
|
||||
public async Task<IActionResult> Report([FromQuery] string format = "pdf", CancellationToken ct = default)
|
||||
{
|
||||
var fmt = format.ToLowerInvariant() switch
|
||||
{
|
||||
"csv" => ExportFormat.Csv,
|
||||
"json" => ExportFormat.Json,
|
||||
_ => ExportFormat.Pdf
|
||||
};
|
||||
var (content, contentType, fileName) = await _export.ExportReportAsync(UserId, fmt, ct);
|
||||
return File(content, contentType, fileName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using InboxIntel.Application.Abstractions;
|
||||
using InboxIntel.Application.DTOs;
|
||||
using InboxIntel.Application.Search;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace InboxIntel.Api.Controllers;
|
||||
|
||||
public class SearchController : ApiControllerBase
|
||||
{
|
||||
private readonly ISearchService _search;
|
||||
public SearchController(ISearchService search) => _search = search;
|
||||
|
||||
/// <summary>Structured search via JSON body.</summary>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Search([FromBody] SearchRequestDto request, CancellationToken ct)
|
||||
=> Ok(await _search.SearchAsync(UserId, request, ct));
|
||||
|
||||
/// <summary>Gmail-like query string search, e.g. ?q=from:github.com is:unread.</summary>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Query([FromQuery] string? q, [FromQuery] int page = 1, [FromQuery] int pageSize = 50, CancellationToken ct = default)
|
||||
{
|
||||
var parsed = GmailQueryParser.Parse(q, page, pageSize);
|
||||
return Ok(await _search.SearchAsync(UserId, parsed, ct));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using InboxIntel.Application.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace InboxIntel.Api.Controllers;
|
||||
|
||||
public class SyncController : ApiControllerBase
|
||||
{
|
||||
private readonly ISyncService _sync;
|
||||
public SyncController(ISyncService sync) => _sync = sync;
|
||||
|
||||
[HttpGet("status")]
|
||||
public async Task<IActionResult> Status(CancellationToken ct)
|
||||
=> Ok(new { status = (await _sync.GetStatusAsync(UserId, ct)).ToString() });
|
||||
|
||||
/// <summary>Triggers a full inbox sync (runs in the background task queue in production).</summary>
|
||||
[HttpPost("full")]
|
||||
public async Task<IActionResult> Full(CancellationToken ct)
|
||||
{
|
||||
await _sync.RunFullSyncAsync(UserId, ct);
|
||||
return Accepted();
|
||||
}
|
||||
|
||||
[HttpPost("incremental")]
|
||||
public async Task<IActionResult> Incremental(CancellationToken ct)
|
||||
{
|
||||
await _sync.RunIncrementalSyncAsync(UserId, ct);
|
||||
return Accepted();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using InboxIntel.Application.Abstractions;
|
||||
using InboxIntel.Application.DTOs;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace InboxIntel.Api.Controllers;
|
||||
|
||||
public class UnsubscribeController : ApiControllerBase
|
||||
{
|
||||
private readonly IUnsubscribeService _unsub;
|
||||
public UnsubscribeController(IUnsubscribeService unsub) => _unsub = unsub;
|
||||
|
||||
[HttpPost("detect")]
|
||||
public async Task<IActionResult> Detect(CancellationToken ct)
|
||||
{
|
||||
await _unsub.DetectAsync(UserId, ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>Senders that are safe to unsubscribe from, ranked by volume.</summary>
|
||||
[HttpGet("safe-list")]
|
||||
public async Task<IActionResult> SafeList(CancellationToken ct)
|
||||
=> Ok(await _unsub.GetSafeToUnsubscribeAsync(UserId, ct));
|
||||
|
||||
/// <summary>Process the confirmed unsubscribe queue. Requires Confirmed = true.</summary>
|
||||
[HttpPost("process")]
|
||||
public async Task<IActionResult> Process([FromBody] UnsubscribeRequestDto request, CancellationToken ct)
|
||||
{
|
||||
var result = await _unsub.ProcessQueueAsync(UserId, request, ct);
|
||||
return result.Succeeded ? Ok(result.Value) : BadRequest(new { error = result.Error });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using InboxIntel.Application.Abstractions;
|
||||
using InboxIntel.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace InboxIntel.Api.Controllers;
|
||||
|
||||
public record WidgetLayoutDto(string WidgetKey, int X, int Y, int W, int H, bool Visible, int SortOrder, string? SettingsJson);
|
||||
|
||||
/// <summary>Persists the user's draggable/resizable dashboard layout.</summary>
|
||||
public class WidgetLayoutController : ApiControllerBase
|
||||
{
|
||||
private readonly IAppDbContext _db;
|
||||
public WidgetLayoutController(IAppDbContext db) => _db = db;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Get(CancellationToken ct)
|
||||
{
|
||||
var layouts = await _db.WidgetLayouts
|
||||
.Where(w => w.UserId == UserId)
|
||||
.OrderBy(w => w.SortOrder)
|
||||
.Select(w => new WidgetLayoutDto(w.WidgetKey, w.X, w.Y, w.W, w.H, w.Visible, w.SortOrder, w.SettingsJson))
|
||||
.ToListAsync(ct);
|
||||
return Ok(layouts);
|
||||
}
|
||||
|
||||
/// <summary>Upserts the full layout for the user (replace semantics).</summary>
|
||||
[HttpPut]
|
||||
public async Task<IActionResult> Save([FromBody] List<WidgetLayoutDto> layout, CancellationToken ct)
|
||||
{
|
||||
var existing = await _db.WidgetLayouts.Where(w => w.UserId == UserId).ToListAsync(ct);
|
||||
var byKey = existing.ToDictionary(w => w.WidgetKey);
|
||||
|
||||
foreach (var dto in layout)
|
||||
{
|
||||
if (byKey.TryGetValue(dto.WidgetKey, out var w))
|
||||
{
|
||||
w.X = dto.X; w.Y = dto.Y; w.W = dto.W; w.H = dto.H;
|
||||
w.Visible = dto.Visible; w.SortOrder = dto.SortOrder; w.SettingsJson = dto.SettingsJson;
|
||||
}
|
||||
else
|
||||
{
|
||||
_db.WidgetLayouts.Add(new WidgetLayout
|
||||
{
|
||||
UserId = UserId, WidgetKey = dto.WidgetKey, X = dto.X, Y = dto.Y, W = dto.W, H = dto.H,
|
||||
Visible = dto.Visible, SortOrder = dto.SortOrder, SettingsJson = dto.SettingsJson
|
||||
});
|
||||
}
|
||||
}
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user