Private
Public Access
1
0

Initial commit: Purrse personal finance app

Self-hosted, plugin-extensible personal finance manager built with
ASP.NET Core 9.0, Vue 3 + Vuetify 3, and PostgreSQL 17.

Backend (8 .NET projects):
- Core: 19 domain models, 6 enums, 14 DTOs, 12 service interfaces
- Data: EF Core DbContext, 16 entity configurations, category seeder
- API: 14 controllers, 15 services, JWT auth, SignalR, middleware
- Plugins: Abstractions + OFX/CSV/QIF file parsers
- Tests: 28 xUnit tests (fingerprinting, duplicate detection, parsers)

Frontend (Vue 3 + Vuetify 3 + TypeScript):
- 13 views, Pinia stores, Axios API services with JWT interceptors
- Dashboard, accounts, transactions, categories, imports, and more

Deployment:
- Docker Compose (PostgreSQL 17 + .NET API + nginx frontend)
- Auto-migration on startup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-08 08:56:46 -05:00
commit 6520ebf221
169 changed files with 7180 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Purrse.Api.Hubs;
using Purrse.Api.Middleware;
using Purrse.Api.Services;
using Purrse.Core.Interfaces.Services;
using Purrse.Data;
using Purrse.Plugins.Abstractions;
using Purrse.Plugins.CSV;
using Purrse.Plugins.OFX;
using Purrse.Plugins.QIF;
var builder = WebApplication.CreateBuilder(args);
// Database
builder.Services.AddDbContext<PurrseDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
// JWT Authentication
var jwtKey = builder.Configuration["Jwt:Key"] ?? "PurrseDefaultSecretKey_ChangeInProduction_AtLeast32Chars!";
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"] ?? "Purrse",
ValidAudience = builder.Configuration["Jwt:Audience"] ?? "Purrse",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
// SignalR JWT support
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
// CORS
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins(
builder.Configuration.GetSection("Cors:Origins").Get<string[]>() ?? new[] { "http://localhost:5173", "http://localhost:8080" })
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
// Services
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IAccountService, AccountService>();
builder.Services.AddScoped<ITransactionService, TransactionService>();
builder.Services.AddScoped<ICategoryService, CategoryService>();
builder.Services.AddScoped<IPayeeService, PayeeService>();
builder.Services.AddScoped<IBudgetService, BudgetService>();
builder.Services.AddScoped<IScheduledTransactionService, ScheduledTransactionService>();
builder.Services.AddScoped<IImportService, ImportService>();
builder.Services.AddScoped<ILoanService, LoanService>();
builder.Services.AddScoped<IDashboardService, DashboardService>();
builder.Services.AddScoped<IReportService, ReportService>();
builder.Services.AddScoped<IDuplicateDetectionService, DuplicateDetectionService>();
// Built-in file parsers
builder.Services.AddSingleton<IFileParser, OFXParserPlugin>();
builder.Services.AddSingleton<IFileParser, CSVParserPlugin>();
builder.Services.AddSingleton<IFileParser, QIFParserPlugin>();
// Plugin system
builder.Services.AddSingleton<PluginService>();
// Background services
builder.Services.AddHostedService<ScheduledTransactionProcessor>();
builder.Services.AddHostedService<FileWatcherService>();
// SignalR
builder.Services.AddSignalR();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
// Auto-migrate database
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<PurrseDbContext>();
await db.Database.MigrateAsync();
}
// Initialize plugin system
var pluginService = app.Services.GetRequiredService<PluginService>();
await pluginService.DiscoverAndLoadPluginsAsync();
// Middleware
app.UseMiddleware<RequestLoggingMiddleware>();
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHub<NotificationHub>("/hubs/notifications");
app.Run();