chore: init project
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
using System.Security.Claims;
|
||||
using InboxIntel.Application.Abstractions;
|
||||
|
||||
namespace InboxIntel.Api.Auth;
|
||||
|
||||
/// <summary>Resolves the authenticated user's id from the cookie principal.</summary>
|
||||
public class CurrentUser : ICurrentUser
|
||||
{
|
||||
private readonly IHttpContextAccessor _accessor;
|
||||
public CurrentUser(IHttpContextAccessor accessor) => _accessor = accessor;
|
||||
|
||||
public bool IsAuthenticated => _accessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false;
|
||||
|
||||
public Guid UserId
|
||||
{
|
||||
get
|
||||
{
|
||||
var raw = _accessor.HttpContext?.User?.FindFirstValue("inboxintel:uid");
|
||||
return Guid.TryParse(raw, out var id) ? id : Guid.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Security.Claims;
|
||||
using InboxIntel.Application.Abstractions;
|
||||
using InboxIntel.Domain.Entities;
|
||||
using InboxIntel.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authentication.OAuth;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace InboxIntel.Api.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// On successful Google sign-in: upsert the user, encrypt and persist the
|
||||
/// refresh token, and attach the internal user id as a claim so downstream
|
||||
/// requests resolve the correct tenant.
|
||||
/// </summary>
|
||||
public static class GoogleAuthEvents
|
||||
{
|
||||
public static async Task OnCreatingTicketAsync(OAuthCreatingTicketContext context)
|
||||
{
|
||||
var sp = context.HttpContext.RequestServices;
|
||||
var db = sp.GetRequiredService<AppDbContext>();
|
||||
var protector = sp.GetRequiredService<ITokenProtector>();
|
||||
|
||||
var principal = context.Principal!;
|
||||
var sub = principal.FindFirstValue(ClaimTypes.NameIdentifier)!;
|
||||
var email = principal.FindFirstValue(ClaimTypes.Email) ?? string.Empty;
|
||||
var name = principal.FindFirstValue(ClaimTypes.Name);
|
||||
var refreshToken = context.RefreshToken;
|
||||
|
||||
var user = await db.Users.FirstOrDefaultAsync(u => u.GoogleSubjectId == sub);
|
||||
if (user is null)
|
||||
{
|
||||
user = new User { GoogleSubjectId = sub, Email = email, DisplayName = name };
|
||||
db.Users.Add(user);
|
||||
}
|
||||
else
|
||||
{
|
||||
user.Email = email;
|
||||
user.DisplayName = name;
|
||||
}
|
||||
|
||||
// Only overwrite the stored token when Google returns a new one.
|
||||
if (!string.IsNullOrEmpty(refreshToken))
|
||||
user.EncryptedRefreshToken = protector.Protect(refreshToken);
|
||||
|
||||
user.AccessTokenExpiresAtUtc = context.ExpiresIn is { } exp
|
||||
? DateTimeOffset.UtcNow.Add(exp) : null;
|
||||
user.LastLoginUtc = DateTimeOffset.UtcNow;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Internal id used by ICurrentUser.
|
||||
context.Identity!.AddClaim(new Claim("inboxintel:uid", user.Id.ToString()));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# Multi-stage build for the ASP.NET Core API.
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy solution + project files first for layer-cached restore.
|
||||
COPY Directory.Build.props ./
|
||||
COPY src/InboxIntel.Domain/InboxIntel.Domain.csproj src/InboxIntel.Domain/
|
||||
COPY src/InboxIntel.Application/InboxIntel.Application.csproj src/InboxIntel.Application/
|
||||
COPY src/InboxIntel.Infrastructure/InboxIntel.Infrastructure.csproj src/InboxIntel.Infrastructure/
|
||||
COPY src/InboxIntel.Api/InboxIntel.Api.csproj src/InboxIntel.Api/
|
||||
RUN dotnet restore src/InboxIntel.Api/InboxIntel.Api.csproj
|
||||
|
||||
COPY src/ src/
|
||||
RUN dotnet publish src/InboxIntel.Api/InboxIntel.Api.csproj -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["dotnet", "InboxIntel.Api.dll"]
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<RootNamespace>InboxIntel.Api</RootNamespace>
|
||||
<AssemblyName>InboxIntel.Api</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\InboxIntel.Application\InboxIntel.Application.csproj" />
|
||||
<ProjectReference Include="..\InboxIntel.Infrastructure\InboxIntel.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,100 @@
|
||||
using System.Security.Claims;
|
||||
using Asp.Versioning;
|
||||
using InboxIntel.Api.Auth;
|
||||
using InboxIntel.Application;
|
||||
using InboxIntel.Application.Abstractions;
|
||||
using InboxIntel.Infrastructure;
|
||||
using InboxIntel.Infrastructure.Configuration;
|
||||
using InboxIntel.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.Google;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Serilog;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Structured logging (Serilog). Note: token values are never logged.
|
||||
builder.Host.UseSerilog((ctx, cfg) => cfg
|
||||
.ReadFrom.Configuration(ctx.Configuration)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console());
|
||||
|
||||
// Persist Data Protection keys so encrypted refresh tokens survive restarts.
|
||||
builder.Services.AddDataProtection()
|
||||
.PersistKeysToFileSystem(new DirectoryInfo(builder.Configuration["DataProtection:KeyPath"] ?? "/keys"))
|
||||
.SetApplicationName("InboxIntel");
|
||||
|
||||
builder.Services.AddApplication();
|
||||
builder.Services.AddInfrastructure(builder.Configuration);
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<ICurrentUser, CurrentUser>();
|
||||
|
||||
// Authentication: cookie session established via Google OAuth2 (only login method).
|
||||
var google = builder.Configuration.GetSection(GoogleOAuthOptions.SectionName).Get<GoogleOAuthOptions>() ?? new();
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddCookie(options =>
|
||||
{
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.Cookie.SameSite = SameSiteMode.Lax;
|
||||
options.ExpireTimeSpan = TimeSpan.FromDays(7);
|
||||
})
|
||||
.AddGoogle(options =>
|
||||
{
|
||||
options.ClientId = google.ClientId;
|
||||
options.ClientSecret = google.ClientSecret;
|
||||
options.AccessType = "offline"; // request a refresh token
|
||||
options.SaveTokens = true;
|
||||
foreach (var scope in google.Scopes) options.Scope.Add(scope);
|
||||
options.Events.OnCreatingTicket = GoogleAuthEvents.OnCreatingTicketAsync;
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
builder.Services.AddApiVersioning(o =>
|
||||
{
|
||||
o.DefaultApiVersion = new ApiVersion(1, 0);
|
||||
o.AssumeDefaultVersionWhenUnspecified = true;
|
||||
o.ReportApiVersions = true;
|
||||
o.ApiVersionReader = new UrlSegmentApiVersionReader();
|
||||
}).AddApiExplorer(o => { o.GroupNameFormat = "'v'VVV"; o.SubstituteApiVersionInUrl = true; });
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
builder.Services.AddCors(o => o.AddPolicy("frontend", p => p
|
||||
.WithOrigins(builder.Configuration.GetSection("Cors:Origins").Get<string[]>() ?? new[] { "http://localhost:5173" })
|
||||
.AllowAnyHeader().AllowAnyMethod().AllowCredentials()));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Apply migrations on startup so `docker-compose up` yields a ready schema.
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
if (db.Database.IsRelational() && app.Configuration.GetValue("Database:AutoMigrate", true))
|
||||
await db.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseSerilogRequestLogging();
|
||||
app.UseCors("frontend");
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program { }
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5080",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"DataProtection": {
|
||||
"KeyPath": "./keys"
|
||||
},
|
||||
"Serilog": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Debug"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Postgres": "Host=localhost;Port=5432;Database=inboxintel;Username=inboxintel;Password=inboxintel"
|
||||
},
|
||||
"Database": {
|
||||
"AutoMigrate": true
|
||||
},
|
||||
"DataProtection": {
|
||||
"KeyPath": "/keys"
|
||||
},
|
||||
"GoogleOAuth": {
|
||||
"ClientId": "",
|
||||
"ClientSecret": "",
|
||||
"Scopes": [
|
||||
"openid",
|
||||
"email",
|
||||
"profile",
|
||||
"https://www.googleapis.com/auth/gmail.readonly",
|
||||
"https://www.googleapis.com/auth/gmail.modify"
|
||||
]
|
||||
},
|
||||
"GmailSync": {
|
||||
"PageSize": 100,
|
||||
"MaxParallelism": 4,
|
||||
"MaxRetries": 5,
|
||||
"BackoffBaseMs": 500,
|
||||
"DailySyncHourUtc": 3
|
||||
},
|
||||
"Ai": {
|
||||
"Mode": "Disabled",
|
||||
"OllamaBaseUrl": "http://localhost:11434",
|
||||
"OllamaModel": "llama3.1",
|
||||
"OpenAiApiKey": "",
|
||||
"OpenAiModel": "gpt-4o-mini"
|
||||
},
|
||||
"Cors": {
|
||||
"Origins": [ "http://localhost:5173" ]
|
||||
},
|
||||
"Serilog": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Warning"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user