Compare commits

..
Author SHA1 Message Date
Stevan Freeborn eef410d71b chore: run dotnet format 2026-02-02 15:46:12 -06:00
Stevan Freeborn fb7750328b feat(infra): add indexes with uniqueness 2026-02-02 15:45:44 -06:00
Stevan Freeborn 35557a8aa7 feat(admincli): add command for creating a user 2026-02-02 15:41:47 -06:00
Stevan Freeborn 5a5efbefd7 tests(api): add integration test for happy path when logging in 2026-02-02 13:33:23 -06:00
Stevan Freeborn dcc5b2e0f0 feat(core): add password hashing and token generation interfaces 2026-02-02 13:19:36 -06:00
Stevan Freeborn 7d5fd4149c test: update infra tests for new authentication namespaces 2026-02-02 13:18:41 -06:00
Stevan Freeborn abc6a7655e test: add moq and code coverage support to test suite 2026-02-02 13:18:28 -06:00
Stevan Freeborn 178f03459b feat(api): integrate abstracted token generation and update login endpoint 2026-02-02 13:18:15 -06:00
Stevan Freeborn b3bfa64b96 feat(infra): move authentication implementations and add ITokenGenerator 2026-02-02 13:17:59 -06:00
Stevan Freeborn 098aa23c62 refactor(core): encapsulate user and refresh token creation with factory methods 2026-02-02 13:17:43 -06:00
Stevan Freeborn 815b6aa57c chore: add todo with psuedo code 2026-02-02 07:00:37 -06:00
Stevan Freeborn 3a1a7138c5 feat(infra): update refresh token model 2026-02-02 07:00:28 -06:00
Stevan Freeborn ee6abc4559 chore: add example jwt options in example settings file 2026-02-02 06:54:39 -06:00
Stevan Freeborn b535c0a121 feat: implement token generation and issuing on successful login 2026-02-02 06:32:14 -06:00
Stevan Freeborn 822da7368c tests: refactor test suite to match new project structure 2026-02-02 04:49:00 -06:00
Stevan Freeborn 7c6e43d62d feat(api): integrate infra and implement password verification on login 2026-02-02 04:48:42 -06:00
Stevan Freeborn 48913fc434 feat: add timestamp interceptor and infrastructure service registration 2026-02-02 04:48:10 -06:00
Stevan Freeborn c01d0b5daa refactor(infra): migrate data access and identity impelementations to infrastructure 2026-02-02 04:47:32 -06:00
Stevan Freeborn 7a7ab90155 refactor(core): move identity models from API to Core 2026-02-02 04:45:56 -06:00
Stevan Freeborn 81e5b9ed6a feat(core): add base entity and password hashing abstractions 2026-02-02 04:45:17 -06:00
Stevan Freeborn b5ea9a514f feat: initialize core, infra, and admin cli projects 2026-02-02 04:44:13 -06:00
Stevan Freeborn c8c45f6a83 feat: implement password hasher 2026-02-01 11:32:14 -06:00
Stevan Freeborn ce45244813 chore: move editorconfig to correct location 2026-02-01 11:31:58 -06:00
Stevan Freeborn e479ba4c8a tests: fix integration test setup 2026-02-01 11:31:46 -06:00
Stevan Freeborn bf7b1b3fc8 fix: remove fake auth check 2026-02-01 11:31:25 -06:00
Stevan Freeborn 872eb3e95d feat: wip on testing login flows 2026-02-01 08:07:23 -06:00
Stevan Freeborn 725392227e feat: setup db context with initial models 2026-02-01 06:13:08 -06:00
Stevan Freeborn 093ee2c357 feat: add simple check for username and password 2026-02-01 06:12:49 -06:00
Stevan Freeborn a1d22b0ec2 chore: add example appsettings file 2026-02-01 06:12:25 -06:00
Stevan Freeborn 2b91e3b791 refactor: use ivalidatableobject api 2026-02-01 06:12:09 -06:00
Stevan Freeborn 8e40b15cb2 chore: update editorconfig settings 2026-02-01 06:11:31 -06:00
Stevan Freeborn 8012ecaa73 feat: wip on login endpoint
- added basic REPR pattern
- added basic vert architecture
- implemented basica validation for single test case
2026-02-01 04:27:06 -06:00
185 changed files with 290 additions and 62841 deletions
-3
View File
@@ -1,3 +0,0 @@
{
"appHostPath": "../src/FiscalOS.AppHost/FiscalOS.AppHost.csproj"
}
-11
View File
@@ -1,11 +0,0 @@
{
"recommendations": [
"Vue.volar",
"vitest.explorer",
"ms-playwright.playwright",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"oxc.oxc-vscode",
"esbenp.prettier-vscode"
]
}
-14
View File
@@ -1,14 +0,0 @@
{
"dotnet.defaultSolution": "FiscalOS.slnx",
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"tsconfig.json": "tsconfig.*.json, env.d.ts, typed-router.d.ts",
"vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*",
"package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .oxlint*, oxlint*, .oxfmt*, .prettier*, prettier*, .editorconfig"
},
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
+116
View File
@@ -0,0 +1,116 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FiscalOS.API.Tests", "tests\FiscalOS.API.Tests\FiscalOS.API.Tests.csproj", "{1869E249-07F6-41CD-B143-AF86F022E4A6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FiscalOS.API", "src\FiscalOS.API\FiscalOS.API.csproj", "{85E0C8F3-AE69-4012-836C-759E9D297762}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FiscalOS.AdminCLI", "src\FiscalOS.AdminCLI\FiscalOS.AdminCLI.csproj", "{C46818A0-1A92-4E4F-8390-48CBA9971745}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FiscalOS.Core", "src\FiscalOS.Core\FiscalOS.Core.csproj", "{612496CD-1F25-4804-8C52-49E62D01FFDC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FiscalOS.Infra", "src\FiscalOS.Infra\FiscalOS.Infra.csproj", "{DAB42870-02A2-4ECB-B07F-6815C52D2435}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FiscalOS.Infra.Tests", "tests\FiscalOS.Infra.Tests\FiscalOS.Infra.Tests.csproj", "{48E0737E-0200-468E-ADA7-75893935DBC0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1869E249-07F6-41CD-B143-AF86F022E4A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1869E249-07F6-41CD-B143-AF86F022E4A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1869E249-07F6-41CD-B143-AF86F022E4A6}.Debug|x64.ActiveCfg = Debug|Any CPU
{1869E249-07F6-41CD-B143-AF86F022E4A6}.Debug|x64.Build.0 = Debug|Any CPU
{1869E249-07F6-41CD-B143-AF86F022E4A6}.Debug|x86.ActiveCfg = Debug|Any CPU
{1869E249-07F6-41CD-B143-AF86F022E4A6}.Debug|x86.Build.0 = Debug|Any CPU
{1869E249-07F6-41CD-B143-AF86F022E4A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1869E249-07F6-41CD-B143-AF86F022E4A6}.Release|Any CPU.Build.0 = Release|Any CPU
{1869E249-07F6-41CD-B143-AF86F022E4A6}.Release|x64.ActiveCfg = Release|Any CPU
{1869E249-07F6-41CD-B143-AF86F022E4A6}.Release|x64.Build.0 = Release|Any CPU
{1869E249-07F6-41CD-B143-AF86F022E4A6}.Release|x86.ActiveCfg = Release|Any CPU
{1869E249-07F6-41CD-B143-AF86F022E4A6}.Release|x86.Build.0 = Release|Any CPU
{85E0C8F3-AE69-4012-836C-759E9D297762}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{85E0C8F3-AE69-4012-836C-759E9D297762}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85E0C8F3-AE69-4012-836C-759E9D297762}.Debug|x64.ActiveCfg = Debug|Any CPU
{85E0C8F3-AE69-4012-836C-759E9D297762}.Debug|x64.Build.0 = Debug|Any CPU
{85E0C8F3-AE69-4012-836C-759E9D297762}.Debug|x86.ActiveCfg = Debug|Any CPU
{85E0C8F3-AE69-4012-836C-759E9D297762}.Debug|x86.Build.0 = Debug|Any CPU
{85E0C8F3-AE69-4012-836C-759E9D297762}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85E0C8F3-AE69-4012-836C-759E9D297762}.Release|Any CPU.Build.0 = Release|Any CPU
{85E0C8F3-AE69-4012-836C-759E9D297762}.Release|x64.ActiveCfg = Release|Any CPU
{85E0C8F3-AE69-4012-836C-759E9D297762}.Release|x64.Build.0 = Release|Any CPU
{85E0C8F3-AE69-4012-836C-759E9D297762}.Release|x86.ActiveCfg = Release|Any CPU
{85E0C8F3-AE69-4012-836C-759E9D297762}.Release|x86.Build.0 = Release|Any CPU
{C46818A0-1A92-4E4F-8390-48CBA9971745}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C46818A0-1A92-4E4F-8390-48CBA9971745}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C46818A0-1A92-4E4F-8390-48CBA9971745}.Debug|x64.ActiveCfg = Debug|Any CPU
{C46818A0-1A92-4E4F-8390-48CBA9971745}.Debug|x64.Build.0 = Debug|Any CPU
{C46818A0-1A92-4E4F-8390-48CBA9971745}.Debug|x86.ActiveCfg = Debug|Any CPU
{C46818A0-1A92-4E4F-8390-48CBA9971745}.Debug|x86.Build.0 = Debug|Any CPU
{C46818A0-1A92-4E4F-8390-48CBA9971745}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C46818A0-1A92-4E4F-8390-48CBA9971745}.Release|Any CPU.Build.0 = Release|Any CPU
{C46818A0-1A92-4E4F-8390-48CBA9971745}.Release|x64.ActiveCfg = Release|Any CPU
{C46818A0-1A92-4E4F-8390-48CBA9971745}.Release|x64.Build.0 = Release|Any CPU
{C46818A0-1A92-4E4F-8390-48CBA9971745}.Release|x86.ActiveCfg = Release|Any CPU
{C46818A0-1A92-4E4F-8390-48CBA9971745}.Release|x86.Build.0 = Release|Any CPU
{612496CD-1F25-4804-8C52-49E62D01FFDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{612496CD-1F25-4804-8C52-49E62D01FFDC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{612496CD-1F25-4804-8C52-49E62D01FFDC}.Debug|x64.ActiveCfg = Debug|Any CPU
{612496CD-1F25-4804-8C52-49E62D01FFDC}.Debug|x64.Build.0 = Debug|Any CPU
{612496CD-1F25-4804-8C52-49E62D01FFDC}.Debug|x86.ActiveCfg = Debug|Any CPU
{612496CD-1F25-4804-8C52-49E62D01FFDC}.Debug|x86.Build.0 = Debug|Any CPU
{612496CD-1F25-4804-8C52-49E62D01FFDC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{612496CD-1F25-4804-8C52-49E62D01FFDC}.Release|Any CPU.Build.0 = Release|Any CPU
{612496CD-1F25-4804-8C52-49E62D01FFDC}.Release|x64.ActiveCfg = Release|Any CPU
{612496CD-1F25-4804-8C52-49E62D01FFDC}.Release|x64.Build.0 = Release|Any CPU
{612496CD-1F25-4804-8C52-49E62D01FFDC}.Release|x86.ActiveCfg = Release|Any CPU
{612496CD-1F25-4804-8C52-49E62D01FFDC}.Release|x86.Build.0 = Release|Any CPU
{DAB42870-02A2-4ECB-B07F-6815C52D2435}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DAB42870-02A2-4ECB-B07F-6815C52D2435}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DAB42870-02A2-4ECB-B07F-6815C52D2435}.Debug|x64.ActiveCfg = Debug|Any CPU
{DAB42870-02A2-4ECB-B07F-6815C52D2435}.Debug|x64.Build.0 = Debug|Any CPU
{DAB42870-02A2-4ECB-B07F-6815C52D2435}.Debug|x86.ActiveCfg = Debug|Any CPU
{DAB42870-02A2-4ECB-B07F-6815C52D2435}.Debug|x86.Build.0 = Debug|Any CPU
{DAB42870-02A2-4ECB-B07F-6815C52D2435}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DAB42870-02A2-4ECB-B07F-6815C52D2435}.Release|Any CPU.Build.0 = Release|Any CPU
{DAB42870-02A2-4ECB-B07F-6815C52D2435}.Release|x64.ActiveCfg = Release|Any CPU
{DAB42870-02A2-4ECB-B07F-6815C52D2435}.Release|x64.Build.0 = Release|Any CPU
{DAB42870-02A2-4ECB-B07F-6815C52D2435}.Release|x86.ActiveCfg = Release|Any CPU
{DAB42870-02A2-4ECB-B07F-6815C52D2435}.Release|x86.Build.0 = Release|Any CPU
{48E0737E-0200-468E-ADA7-75893935DBC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{48E0737E-0200-468E-ADA7-75893935DBC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{48E0737E-0200-468E-ADA7-75893935DBC0}.Debug|x64.ActiveCfg = Debug|Any CPU
{48E0737E-0200-468E-ADA7-75893935DBC0}.Debug|x64.Build.0 = Debug|Any CPU
{48E0737E-0200-468E-ADA7-75893935DBC0}.Debug|x86.ActiveCfg = Debug|Any CPU
{48E0737E-0200-468E-ADA7-75893935DBC0}.Debug|x86.Build.0 = Debug|Any CPU
{48E0737E-0200-468E-ADA7-75893935DBC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{48E0737E-0200-468E-ADA7-75893935DBC0}.Release|Any CPU.Build.0 = Release|Any CPU
{48E0737E-0200-468E-ADA7-75893935DBC0}.Release|x64.ActiveCfg = Release|Any CPU
{48E0737E-0200-468E-ADA7-75893935DBC0}.Release|x64.Build.0 = Release|Any CPU
{48E0737E-0200-468E-ADA7-75893935DBC0}.Release|x86.ActiveCfg = Release|Any CPU
{48E0737E-0200-468E-ADA7-75893935DBC0}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{1869E249-07F6-41CD-B143-AF86F022E4A6} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{85E0C8F3-AE69-4012-836C-759E9D297762} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{C46818A0-1A92-4E4F-8390-48CBA9971745} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{612496CD-1F25-4804-8C52-49E62D01FFDC} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{DAB42870-02A2-4ECB-B07F-6815C52D2435} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{48E0737E-0200-468E-ADA7-75893935DBC0} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
-21
View File
@@ -1,21 +0,0 @@
<Solution>
<Configurations>
<Platform Name="Any CPU" />
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Folder Name="/src/">
<Project Path="src/FiscalOS.AdminCLI/FiscalOS.AdminCLI.csproj" />
<Project Path="src/FiscalOS.API/FiscalOS.API.csproj" />
<Project Path="src/FiscalOS.Core/FiscalOS.Core.csproj" />
<Project Path="src/FiscalOS.Infra/FiscalOS.Infra.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/FiscalOS.API.Tests/FiscalOS.API.Tests.csproj" />
<Project Path="tests/FiscalOS.AppHost.Tests/FiscalOS.AppHost.Tests.csproj" />
<Project Path="tests/FiscalOS.Core.Tests/FiscalOS.Core.Tests.csproj" />
<Project Path="tests/FiscalOS.Infra.Tests/FiscalOS.Infra.Tests.csproj" />
</Folder>
<Project Path="src/FiscalOS.AppHost/FiscalOS.AppHost.csproj" />
<Project Path="src/FiscalOS.ServiceDefaults/FiscalOS.ServiceDefaults.csproj" />
</Solution>
+2 -11
View File
@@ -3,9 +3,8 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Aspire.Hosting.JavaScript" Version="13.1.1" />
<PackageVersion Include="Going.Plaid" Version="6.56.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
@@ -13,13 +12,5 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
<PackageVersion Include="Spectre.Console" Version="0.54.0" />
<PackageVersion Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.1.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.1.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.1.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.14.0" />
</ItemGroup>
</Project>
</Project>
@@ -1,16 +0,0 @@
namespace FiscalOS.API.Accounts;
internal static class AccountsExtensions
{
private const string RouteGroupPrefix = "/accounts";
public static RouteGroupBuilder MapAccountsEndpoints(this WebApplication app)
{
var accountsGroup = app.MapGroup(RouteGroupPrefix)
.RequireAuthorization();
accountsGroup.MapAddEndpoint();
return accountsGroup;
}
}
-56
View File
@@ -1,56 +0,0 @@
namespace FiscalOS.API.Accounts.Add;
internal static class Endpoint
{
private const string Route = "/";
public static RouteHandlerBuilder MapAddEndpoint(this RouteGroupBuilder groupBuilder)
{
return groupBuilder.MapPost(Route, HandleAsync);
}
private static async Task<IResult> HandleAsync(
HttpContext httpContext,
[FromBody] Request request,
[FromServices] AppDbContext appDbContext,
CancellationToken ct
)
{
var userId = httpContext.GetUserId();
var user = await appDbContext.Users
.Include(u => u.Institutions.Where(i => i.Metadata is PlaidMetadata && ((PlaidMetadata)i.Metadata).PlaidId == request.PlaidInstitutionId))
.Include(u => u.Accounts.Where(a => a.Metadata is PlaidAccountMetadata && ((PlaidAccountMetadata)a.Metadata).PlaidId == request.PlaidAccountId))
.ThenInclude(a => a.Metadata)
.SingleOrDefaultAsync(u => u.Id == userId, ct);
if (user is null)
{
return Results.Unauthorized();
}
if (user.Institutions.Any() is false)
{
return Results.ValidationProblem(new Dictionary<string, string[]>
{
["PlaidInstitutionId"] = ["The PlaidInstitutionId field is invalid. No institution connected with the given PlaidInstitutionId was found for the user."],
});
}
if (user.Accounts.Any())
{
return Results.Conflict();
}
var accountMetadata = PlaidAccountMetadata.From(request.PlaidAccountId, request.PlaidAccountName);
var account = Account.From(user.Institutions.First().Id, request.PlaidAccountName, accountMetadata);
var balance = Balance.From(request.AccountCurrentBalance, request.AccountAvailableBalance, request.AccountCurrencyCode);
account.AddBalance(balance);
user.AddAccount(account);
await appDbContext.SaveChangesAsync(ct);
return Results.Ok();
}
}
-38
View File
@@ -1,38 +0,0 @@
namespace FiscalOS.API.Accounts.Add;
public record Request : IValidatableObject
{
public string PlaidInstitutionId { get; init; } = string.Empty;
public string PlaidAccountId { get; init; } = string.Empty;
public string PlaidAccountName { get; init; } = string.Empty;
public decimal AccountCurrentBalance { get; init; }
public decimal AccountAvailableBalance { get; init; }
public string AccountCurrencyCode { get; init; } = string.Empty;
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(PlaidInstitutionId))
{
var fieldName = nameof(PlaidInstitutionId);
yield return new($"The {fieldName} field is required.", [fieldName]);
}
if (string.IsNullOrWhiteSpace(PlaidAccountId))
{
var fieldName = nameof(PlaidAccountId);
yield return new($"The {fieldName} field is required.", [fieldName]);
}
if (string.IsNullOrWhiteSpace(PlaidAccountName))
{
var fieldName = nameof(PlaidAccountName);
yield return new($"The {fieldName} field is required.", [fieldName]);
}
if (string.IsNullOrWhiteSpace(AccountCurrencyCode))
{
var fieldName = nameof(AccountCurrencyCode);
yield return new($"The {fieldName} field is required.", [fieldName]);
}
}
}
-18
View File
@@ -1,18 +0,0 @@
namespace FiscalOS.API.Auth;
internal static class AuthExtensions
{
private const string RouteGroupPrefix = "/auth";
public static RouteGroupBuilder MapAuthEndpoints(this WebApplication app)
{
var authGroup = app.MapGroup(RouteGroupPrefix);
authGroup.MapLoginEndpoint();
authGroup.MapRefreshEndpoint()
.RequireAuthorization(Schemes.AllowExpiredTokens);
return authGroup;
}
}
-60
View File
@@ -1,60 +0,0 @@
namespace FiscalOS.API.Auth.Refresh;
internal static class Endpoint
{
private const string Route = "/refresh";
public static RouteHandlerBuilder MapRefreshEndpoint(this RouteGroupBuilder groupBuilder)
{
return groupBuilder.MapPost(Route, HandleAsync);
}
private static async Task<IResult> HandleAsync(
HttpContext httpContext,
[FromServices] AppDbContext appDbContext,
[FromServices] TimeProvider timeProvider,
[FromServices] ITokenGenerator tokenGenerator
)
{
var requestUserId = httpContext.GetUserId();
var token = httpContext.GetRefreshTokenFromCookie();
var refreshToken = await appDbContext.RefreshTokens
.Include(t => t.User)
.SingleOrDefaultAsync(t => t.Token == token);
if (refreshToken is null || refreshToken.User is null)
{
return Results.BadRequest();
}
if (refreshToken.UserId != requestUserId)
{
await appDbContext.RefreshTokens
.Where(t => t.UserId == refreshToken.UserId)
.ExecuteUpdateAsync(t => t.SetProperty(t => t.Revoked, true));
await appDbContext.SaveChangesAsync();
return Results.Forbid();
}
var now = timeProvider.GetUtcNow();
if (refreshToken.Revoked || refreshToken.IsExpired(now))
{
return Results.BadRequest();
}
var accessToken = tokenGenerator.GenerateAccessToken(refreshToken.User);
var newRefreshToken = tokenGenerator.GenerateRefreshToken(refreshToken.User);
refreshToken.Revoke();
await appDbContext.RefreshTokens.AddAsync(newRefreshToken);
await appDbContext.SaveChangesAsync();
httpContext.SetRefreshTokenCookie(newRefreshToken);
return Results.Ok(Response.From(accessToken));
}
}
-22
View File
@@ -1,22 +0,0 @@
namespace FiscalOS.API.Auth.Refresh;
internal sealed record Response
{
public string AccessToken { get; init; }
[JsonConstructor]
private Response()
{
AccessToken = string.Empty;
}
private Response(string accessToken)
{
AccessToken = accessToken;
}
public static Response From(string accessToken)
{
return new(accessToken);
}
}
+1 -1
View File
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
@@ -10,7 +11,6 @@
<ItemGroup>
<ProjectReference Include="..\FiscalOS.Core\FiscalOS.Core.csproj" />
<ProjectReference Include="..\FiscalOS.Infra\FiscalOS.Infra.csproj" />
<ProjectReference Include="..\FiscalOS.ServiceDefaults\FiscalOS.ServiceDefaults.csproj" />
</ItemGroup>
</Project>
@@ -1,32 +0,0 @@
namespace FiscalOS.API.Http;
internal static class HttpContextExtensions
{
private const string RefreshTokenCookieName = "fiscalos_refresh_cookie";
public static Guid GetUserId(this HttpContext context)
{
var id = context.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? string.Empty;
return Guid.TryParse(id, out var userId) ? userId : Guid.Empty;
}
public static void SetRefreshTokenCookie(this HttpContext context, RefreshToken token)
{
context.Response.Cookies.Append(
RefreshTokenCookieName,
token.Token,
new CookieOptions
{
HttpOnly = true,
Expires = token.ExpiresAt,
SameSite = SameSiteMode.Strict,
Secure = true
}
);
}
public static string GetRefreshTokenFromCookie(this HttpContext context)
{
return context.Request.Cookies[RefreshTokenCookieName] ?? string.Empty;
}
}
@@ -1,57 +0,0 @@
namespace FiscalOS.API.Institutions.Connect;
internal static class Endpoint
{
private const string Route = "/connect";
public static RouteHandlerBuilder MapConnectEndpoint(this RouteGroupBuilder groupBuilder)
{
return groupBuilder.MapPost(Route, HandleAsync);
}
private static async Task<IResult> HandleAsync(
HttpContext httpContext,
[FromBody] Request request,
[FromServices] AppDbContext appDbContext,
[FromServices] PlaidService plaidService,
[FromServices] IEncryptor encryptor,
CancellationToken ct
)
{
var userId = httpContext.GetUserId();
var user = await appDbContext.Users
.Include(u => u.Institutions
.Where(
i => i.Metadata is PlaidMetadata &&
((PlaidMetadata)i.Metadata).PlaidId == request.PlaidInstitutionId
)
)
.ThenInclude(i => i.Metadata)
.SingleOrDefaultAsync(u => u.Id == userId, ct);
if (user is null)
{
return Results.Unauthorized();
}
if (user.Institutions.Any())
{
return Results.Conflict();
}
var (itemId, accessToken) = await plaidService.ExchangeTokenAsync(request.PublicToken);
var item = await plaidService.GetItemAsync(accessToken);
var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, accessToken, ct);
var plaidMetadata = PlaidMetadata.From(item.InstitutionId, item.InstitutionName, encryptedAccessToken);
var institution = Institution.From(item.InstitutionName, plaidMetadata);
user.AddInstitution(institution);
await appDbContext.SaveChangesAsync(ct);
return Results.Ok();
}
}
@@ -1,22 +0,0 @@
namespace FiscalOS.API.Institutions.Connect;
public record Request : IValidatableObject
{
public string PublicToken { get; init; } = string.Empty;
public string PlaidInstitutionId { get; init; } = string.Empty;
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(PublicToken))
{
var fieldName = nameof(PublicToken);
yield return new($"The {fieldName} field is required.", [fieldName]);
}
if (string.IsNullOrWhiteSpace(PlaidInstitutionId))
{
var fieldName = nameof(PlaidInstitutionId);
yield return new($"The {fieldName} field is required.", [fieldName]);
}
}
}
@@ -1,34 +0,0 @@
namespace FiscalOS.API.Institutions.Get;
internal static class Endpoint
{
private const string Route = "/";
public static RouteHandlerBuilder MapGetEndpoint(this RouteGroupBuilder groupBuilder)
{
return groupBuilder.MapGet(Route, HandleAsync);
}
private static async Task<IResult> HandleAsync(
HttpContext httpContext,
[FromServices] AppDbContext appDbContext
)
{
var userId = httpContext.GetUserId();
var user = await appDbContext.Users
.Include(u => u.Institutions)
.FirstOrDefaultAsync(u => u.Id == userId);
if (user is null)
{
return Results.Unauthorized();
}
var institutionDtos = user.Institutions
.Select(InstitutionDto.FromInstitution)
.OrderBy(dto => dto.Name);
return Results.Ok(Response.From(institutionDtos));
}
}
@@ -1,39 +0,0 @@
namespace FiscalOS.API.Institutions.Get;
internal sealed record Response
{
public IEnumerable<InstitutionDto> Institutions { get; init; } = [];
[JsonConstructor]
private Response()
{
}
public static Response From(IEnumerable<InstitutionDto> institutions)
{
return new Response
{
Institutions = institutions,
};
}
}
internal sealed record InstitutionDto
{
public string Id { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
[JsonConstructor]
private InstitutionDto()
{
}
public static InstitutionDto FromInstitution(Institution institution)
{
return new InstitutionDto
{
Id = institution.Id.ToString(),
Name = institution.Name,
};
}
}
@@ -1,51 +0,0 @@
namespace FiscalOS.API.Institutions.GetAvailable;
internal static class Endpoint
{
private const string Route = "/{id}/available";
public static RouteHandlerBuilder MapGetAvailableEndpoint(this RouteGroupBuilder groupBuilder)
{
return groupBuilder.MapGet(Route, HandleAsync);
}
private static async Task<IResult> HandleAsync(
HttpContext httpContext,
[FromRoute] Guid id,
[FromServices] AppDbContext appDbContext,
[FromServices] PlaidService plaidService,
[FromServices] IEncryptor encryptor,
CancellationToken ct
)
{
var userId = httpContext.GetUserId();
var user = await appDbContext.Users
.Include(u => u.Institutions.Where(i => i.Id == id))
.ThenInclude(i => i.Metadata)
.SingleOrDefaultAsync(u => u.Id == userId, ct);
if (user is null)
{
return Results.Unauthorized();
}
if (user.Institutions.Any() is false)
{
return Results.NotFound();
}
var institution = user.Institutions.First();
if (institution.Metadata is not PlaidMetadata plaidMetadata)
{
return Results.BadRequest();
}
var accessToken = await encryptor.DecryptAsyncFor(user, plaidMetadata.EncryptedAccessToken, ct);
var accounts = await plaidService.GetAccountsAsync(accessToken);
var accountsDtos = accounts.Select(AccountDto.FromPlaidAccount);
return Results.Ok(Response.From(accountsDtos));
}
}
@@ -1,45 +0,0 @@
namespace FiscalOS.API.Institutions.GetAvailable;
internal sealed record Response
{
public IEnumerable<AccountDto> Accounts { get; init; } = [];
[JsonConstructor]
private Response()
{
}
public static Response From(IEnumerable<AccountDto> accounts)
{
return new Response
{
Accounts = accounts
};
}
}
internal sealed record AccountDto
{
public string ProviderId { get; init; } = string.Empty;
public string ProviderName { get; init; } = string.Empty;
public decimal CurrentBalance { get; init; }
public decimal AvailableBalance { get; init; }
public string CurrencyCode { get; init; } = string.Empty;
[JsonConstructor]
private AccountDto()
{
}
public static AccountDto FromPlaidAccount(Going.Plaid.Entity.Account plaidAccount)
{
return new AccountDto
{
ProviderId = plaidAccount.AccountId,
ProviderName = plaidAccount.Name,
AvailableBalance = plaidAccount.Available,
CurrentBalance = plaidAccount.Current,
CurrencyCode = plaidAccount.CurrencyCode,
};
}
}
@@ -1,19 +0,0 @@
namespace FiscalOS.API.Institutions;
internal static class InstitutionsExtensions
{
private const string RouteGroupPrefix = "/institutions";
public static RouteGroupBuilder MapInstitutionsEndpoints(this WebApplication app)
{
var institutionsGroup = app.MapGroup(RouteGroupPrefix)
.RequireAuthorization();
institutionsGroup.MapConnectEndpoint();
institutionsGroup.MapGetAvailableEndpoint();
institutionsGroup.MapLinkEndpoint();
institutionsGroup.MapGetEndpoint();
return institutionsGroup;
}
}
@@ -1,31 +0,0 @@
namespace FiscalOS.API.Institutions.Link;
internal static class Endpoint
{
private const string Route = "/link";
public static RouteHandlerBuilder MapLinkEndpoint(this RouteGroupBuilder groupBuilder)
{
return groupBuilder.MapPost(Route, HandleAsync);
}
private static async Task<IResult> HandleAsync(
HttpContext httpContext,
[FromServices] AppDbContext appDbContext,
[FromServices] PlaidService plaidService
)
{
var userId = httpContext.GetUserId();
var user = await appDbContext.Users.SingleOrDefaultAsync(u => u.Id == userId);
if (user is null)
{
return Results.Unauthorized();
}
var linkToken = await plaidService.CreateLinkTokenAsync(user.Id.ToString());
return Results.Ok(Response.From(linkToken));
}
}
@@ -1,19 +0,0 @@
namespace FiscalOS.API.Institutions.Link;
public sealed record Response
{
public string LinkToken { get; init; } = string.Empty;
[JsonConstructor]
private Response()
{
}
public static Response From(string linkToken)
{
return new()
{
LinkToken = linkToken,
};
}
}
@@ -1,17 +1,17 @@
namespace FiscalOS.API.Auth.Login;
namespace FiscalOS.API.Login;
internal static class Endpoint
{
private const string Route = "/login";
public static RouteHandlerBuilder MapLoginEndpoint(this RouteGroupBuilder groupBuilder)
public static RouteHandlerBuilder MapLoginEndpoint(this WebApplication app)
{
return groupBuilder.MapPost(Route, HandleAsync);
return app.MapPost(Route, HandleAsync);
}
private static async Task<IResult> HandleAsync(
HttpContext httpContext,
[FromBody] Request loginRequest,
[FromBody] LoginRequest loginRequest,
[FromServices] AppDbContext appDbContext,
[FromServices] IPasswordHasher passwordHasher,
[FromServices] ITokenGenerator tokenService
@@ -32,13 +32,22 @@ internal static class Endpoint
}
var accessToken = tokenService.GenerateAccessToken(user);
var refreshToken = tokenService.GenerateRefreshToken(user);
user.AddRefreshToken(refreshToken);
await appDbContext.RefreshTokens.AddAsync(refreshToken);
await appDbContext.SaveChangesAsync();
httpContext.SetRefreshTokenCookie(refreshToken);
httpContext.Response.Cookies.Append(
"fiscalos_refresh_cookie",
refreshToken.Token,
new CookieOptions
{
HttpOnly = true,
Expires = refreshToken.ExpiresAt,
SameSite = SameSiteMode.Strict,
Secure = true
}
);
return Results.Ok(Response.From(accessToken));
}
@@ -1,6 +1,7 @@
namespace FiscalOS.API.Auth.Login;
public record Request : IValidatableObject
namespace FiscalOS.API.Login;
public record LoginRequest : IValidatableObject
{
public string Username { get; init; } = string.Empty;
public string Password { get; init; } = string.Empty;
@@ -9,14 +10,12 @@ public record Request : IValidatableObject
{
if (string.IsNullOrWhiteSpace(Username))
{
var fieldName = nameof(Username);
yield return new($"The {fieldName} field is required.", [fieldName]);
yield return new("The Username field is required.", [nameof(Username)]);
}
if (string.IsNullOrWhiteSpace(Password))
{
var fieldName = nameof(Password);
yield return new($"The {fieldName} field is required.", [fieldName]);
yield return new("The Password field is required.", [nameof(Password)]);
}
}
}
@@ -1,4 +1,4 @@
namespace FiscalOS.API.Auth.Login;
namespace FiscalOS.API.Login;
internal sealed record Response
{
+7 -33
View File
@@ -1,45 +1,19 @@
using FiscalOS.ServiceDefaults;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddValidation();
builder.Services.AddProblemDetails();
builder.Services.AddOpenApi();
builder.Services.AddInfrastructure();
builder.Services.AddAuthentication(static o =>
{
o.DefaultAuthenticateScheme = Schemes.Default;
o.DefaultChallengeScheme = Schemes.Default;
})
.AddJwtBearer(Schemes.Default)
.AddJwtBearer(Schemes.AllowExpiredTokens);
builder.Services.AddAuthorizationBuilder()
.AddPolicy(Schemes.Default, static policy =>
{
policy.AuthenticationSchemes = [Schemes.Default];
policy.RequireAuthenticatedUser();
})
.AddPolicy(Schemes.AllowExpiredTokens, static policy =>
{
policy.AuthenticationSchemes = [Schemes.AllowExpiredTokens];
policy.RequireAuthenticatedUser();
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseStatusCodePages();
app.MapDefaultEndpoints();
app.MapAuthEndpoints();
app.MapAccountsEndpoints();
app.MapInstitutionsEndpoints();
app.MapLoginEndpoint();
app.Run();
@@ -18,6 +18,6 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
},
}
}
+1 -17
View File
@@ -1,24 +1,8 @@
global using System.ComponentModel.DataAnnotations;
global using System.Security.Claims;
global using System.Text.Json.Serialization;
global using FiscalOS.API.Institutions.Get;
global using FiscalOS.API.Accounts;
global using FiscalOS.API.Accounts.Add;
global using FiscalOS.API.Auth;
global using FiscalOS.API.Auth.Login;
global using FiscalOS.API.Auth.Refresh;
global using FiscalOS.API.Http;
global using FiscalOS.API.Institutions;
global using FiscalOS.API.Institutions.Connect;
global using FiscalOS.API.Institutions.GetAvailable;
global using FiscalOS.API.Institutions.Link;
global using FiscalOS.Core.Accounts;
global using FiscalOS.API.Login;
global using FiscalOS.Core.Authentication;
global using FiscalOS.Core.Identity;
global using FiscalOS.Core.Security;
global using FiscalOS.Infra.Accounts.Plaid;
global using FiscalOS.Infra.Authentication;
global using FiscalOS.Infra.Data;
global using FiscalOS.Infra.DependencyInjection;
@@ -14,13 +14,5 @@
"Issuer": "Issuer",
"Secret": "Secret",
"ExpiryInMinutes": "ExpiryInMinutes"
},
"FileKeyRingOptions": {
"KeysDirectoryPath": "KeysDirectoryPath",
"PrimaryKeyId": "PrimaryKeyId"
},
"PlaidClientOptions": {
"ClientId": "ClientId",
"Secret": "Secret"
}
}
+2 -10
View File
@@ -3,9 +3,7 @@ namespace FiscalOS.AdminCLI;
internal sealed class App(
IAnsiConsole console,
IPasswordHasher passwordHasher,
IServiceScopeFactory serviceScopeFactory,
IEncryptor encryptor,
IKeyRing keyRing
IServiceScopeFactory serviceScopeFactory
) : IHostedService
{
private readonly IAnsiConsole _console = console;
@@ -34,19 +32,13 @@ internal sealed class App(
);
var hashedPassword = passwordHasher.Hash(password);
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(cancellationToken);
var user = User.From(username, hashedPassword, userEncryptionKey);
var user = User.From(username, hashedPassword);
await appDbContext.Users.AddAsync(user, cancellationToken);
await appDbContext.SaveChangesAsync(cancellationToken);
_console.MarkupLine($"User [green]{username}[/] created successfully.");
break;
case Commands.GenerateKey:
var key = encryptor.GenerateKey();
var keyRingEntry = await keyRing.SaveKeyAsync(key);
_console.MarkupLine($"Key generated with id [green]{keyRingEntry.KeyId}[/] generated successfully.");
break;
case Commands.Exit:
default:
break;
+1 -6
View File
@@ -3,12 +3,7 @@ namespace FiscalOS.AdminCLI;
internal static class Commands
{
public const string CreateUser = "Create User";
public const string GenerateKey = "Generate Key";
public const string Exit = "Exit";
public static readonly string[] All = [
CreateUser,
GenerateKey,
Exit
];
public static readonly string[] All = [CreateUser, Exit];
}
-1
View File
@@ -1,7 +1,6 @@
global using FiscalOS.AdminCLI;
global using FiscalOS.Core.Authentication;
global using FiscalOS.Core.Identity;
global using FiscalOS.Core.Security;
global using FiscalOS.Infra.Data;
global using FiscalOS.Infra.DependencyInjection;
@@ -1,9 +1,5 @@
{
"AppDbContextOptions": {
"DatabaseFilePath": "DatabaseFilePath"
},
"FileKeyRingOptions": {
"KeysDirectoryPath": "KeysDirectoryPath",
"PrimaryKeyId": "PrimaryKeyId"
}
}
-12
View File
@@ -1,12 +0,0 @@
using FiscalOS.AppHost;
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.FiscalOS_API>(ProjectNames.API)
.WithHttpHealthCheck("/health");
builder.AddViteApp(ProjectNames.Web, "../FiscalOS.Web")
.WithReference(api)
.WithHttpsEndpoint(port: 7061, env: "PORT");
builder.Build().Run();
@@ -1,19 +0,0 @@
<Project Sdk="Aspire.AppHost.Sdk/13.1.1">
<ItemGroup>
<ProjectReference Include="..\FiscalOS.API\FiscalOS.API.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.JavaScript" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>80a27726-36cc-4a4d-a55d-11af527158bc</UserSecretsId>
</PropertyGroup>
</Project>
-7
View File
@@ -1,7 +0,0 @@
namespace FiscalOS.AppHost;
public static class ProjectNames
{
public const string API = "api";
public const string Web = "web";
}
@@ -1,31 +0,0 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17104;http://localhost:15068",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21226",
"ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23228",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22081"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15068",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19221",
"ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18206",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20097"
}
}
}
}
-30
View File
@@ -1,30 +0,0 @@
namespace FiscalOS.Core.Accounts;
public sealed class Account : Entity
{
public Guid UserId { get; init; }
public Guid InstitutionId { get; init; }
public Institution? Institution { get; init; }
public string Name { get; init; } = string.Empty;
public AccountMetadata? Metadata { get; init; }
private readonly List<Balance> _balances = [];
public IEnumerable<Balance> Balances => _balances;
public static Account From(Guid institutionId, string name, AccountMetadata accountMetadata)
{
return new()
{
InstitutionId = institutionId,
Name = name,
Metadata = accountMetadata,
};
}
public void AddBalance(Balance balance)
{
ArgumentNullException.ThrowIfNull(balance);
_balances.Add(balance);
}
}
@@ -1,15 +0,0 @@
namespace FiscalOS.Core.Accounts;
public abstract class AccountMetadata : Entity
{
private readonly string _type;
public string Type => _type;
public Guid AccountId { get; init; }
protected AccountMetadata(string type)
{
_type = type;
}
}
-27
View File
@@ -1,27 +0,0 @@
namespace FiscalOS.Core.Accounts;
public sealed class Balance : Entity
{
public Guid AccountId { get; init; }
public decimal Current { get; init; }
public decimal Available { get; init; }
public string CurrencyCode { get; init; } = string.Empty;
private Balance()
{
}
public static Balance From(
decimal current,
decimal available,
string currencyCode
)
{
return new()
{
Current = current,
Available = available,
CurrencyCode = currencyCode,
};
}
}
-24
View File
@@ -1,24 +0,0 @@
namespace FiscalOS.Core.Accounts;
public sealed class Institution : Entity
{
public Guid UserId { get; init; }
public string Name { get; init; } = string.Empty;
public InstitutionMetadata? Metadata { get; init; }
private Institution()
{
}
public static Institution From(string? name, InstitutionMetadata metadata)
{
ArgumentNullException.ThrowIfNull(name, nameof(name));
ArgumentNullException.ThrowIfNull(metadata, nameof(metadata));
return new Institution()
{
Name = name,
Metadata = metadata
};
}
}
@@ -1,15 +0,0 @@
namespace FiscalOS.Core.Accounts;
public abstract class InstitutionMetadata : Entity
{
private readonly string _type;
public string Type => _type;
public Guid InstitutionId { get; init; }
protected InstitutionMetadata(string type)
{
_type = type;
}
}
+1 -25
View File
@@ -5,8 +5,7 @@ public sealed class RefreshToken : Entity
public string Token { get; init; } = string.Empty;
public DateTimeOffset ExpiresAt { get; init; }
public Guid UserId { get; init; }
public User? User { get; init; }
public bool Revoked { get; set; }
public User User { get; init; } = User.New();
private RefreshToken()
{
@@ -21,27 +20,4 @@ public sealed class RefreshToken : Entity
ExpiresAt = expiresAt,
};
}
public static RefreshToken From(User user, string token, DateTimeOffset expiresAt)
{
ArgumentNullException.ThrowIfNull(user, nameof(user));
return new()
{
User = user,
UserId = user.Id,
Token = token,
ExpiresAt = expiresAt,
};
}
public void Revoke()
{
Revoked = true;
}
public bool IsExpired(DateTimeOffset now)
{
return now >= ExpiresAt;
}
}
+2 -38
View File
@@ -4,17 +4,8 @@ public sealed class User : Entity
{
public string Username { get; init; } = string.Empty;
public string HashedPassword { get; init; } = string.Empty;
public string EncryptionKeyId { get; init; } = string.Empty;
public string EncryptedDataKey { get; init; } = string.Empty;
private readonly List<RefreshToken> _refreshTokens = [];
public IEnumerable<RefreshToken> RefreshTokens => _refreshTokens;
private readonly List<Institution> _institutions = [];
public IEnumerable<Institution> Institutions => _institutions;
private readonly List<Account> _accounts = [];
public IEnumerable<Account> Accounts => _accounts;
public ICollection<RefreshToken> RefreshTokens { get; init; } = [];
private User()
{
@@ -25,39 +16,12 @@ public sealed class User : Entity
return new();
}
public static User From(string username, string hashedPassword, EncryptedDataKey encryptedDataKey)
public static User From(string username, string hashedPassword)
{
ArgumentNullException.ThrowIfNull(username, nameof(username));
ArgumentNullException.ThrowIfNull(hashedPassword, nameof(hashedPassword));
ArgumentNullException.ThrowIfNull(encryptedDataKey, nameof(encryptedDataKey));
return new()
{
Username = username,
HashedPassword = hashedPassword,
EncryptedDataKey = encryptedDataKey.EncryptedKey,
EncryptionKeyId = encryptedDataKey.KeyIdUsed,
};
}
public void AddRefreshToken(RefreshToken refreshToken)
{
ArgumentNullException.ThrowIfNull(refreshToken, nameof(refreshToken));
_refreshTokens.Add(refreshToken);
}
public void AddInstitution(Institution institution)
{
ArgumentNullException.ThrowIfNull(institution, nameof(institution));
_institutions.Add(institution);
}
public void AddAccount(Account account)
{
ArgumentNullException.ThrowIfNull(account, nameof(account));
_accounts.Add(account);
}
}
@@ -1,21 +0,0 @@
namespace FiscalOS.Core.Security;
public sealed record EncryptedDataKey
{
public string KeyIdUsed { get; init; }
public string EncryptedKey { get; init; }
private EncryptedDataKey(string keyIdUsed, string encryptedKey)
{
KeyIdUsed = keyIdUsed;
EncryptedKey = encryptedKey;
}
public static EncryptedDataKey From(string keyIdUsed, string encryptedKey)
{
ArgumentNullException.ThrowIfNull(keyIdUsed, nameof(keyIdUsed));
ArgumentNullException.ThrowIfNull(encryptedKey, nameof(encryptedKey));
return new(keyIdUsed, encryptedKey);
}
}
-11
View File
@@ -1,11 +0,0 @@
namespace FiscalOS.Core.Security;
public interface IEncryptor
{
string GenerateKey();
Task<EncryptedDataKey> GenerateEncryptedKeyAsync(CancellationToken ct);
Task<string> EncryptAsync(string plainText, CancellationToken ct);
Task<string> EncryptAsyncFor(User user, string plainText, CancellationToken ct);
Task<string> DecryptAsync(string cipherText, CancellationToken ct);
Task<string> DecryptAsyncFor(User user, string plainText, CancellationToken ct);
}
-8
View File
@@ -1,8 +0,0 @@
namespace FiscalOS.Core.Security;
public interface IKeyRing
{
KeyRingEntry GetKey(string keyId);
KeyRingEntry GetPrimaryKey();
Task<KeyRingEntry> SaveKeyAsync(string key);
}
@@ -1,21 +0,0 @@
namespace FiscalOS.Core.Security;
public sealed record KeyRingEntry
{
public string KeyId { get; init; }
public string Key { get; init; }
private KeyRingEntry(string keyId, string key)
{
KeyId = keyId;
Key = key;
}
public static KeyRingEntry From(string keyId, string key)
{
ArgumentNullException.ThrowIfNull(keyId, nameof(keyId));
ArgumentNullException.ThrowIfNull(key, nameof(key));
return new(keyId, key);
}
}
+2 -3
View File
@@ -1,4 +1,3 @@
global using FiscalOS.Core.Accounts;
global using FiscalOS.Core.Data;
global using FiscalOS.Core.Identity;
global using FiscalOS.Core.Security;
global using FiscalOS.Core.Identity;
-1
View File
@@ -1,4 +1,3 @@
[*.cs]
dotnet_diagnostic.CA1062.severity = none
dotnet_diagnostic.CA5404.severity = none
@@ -1,21 +0,0 @@
namespace FiscalOS.Infra.Accounts.Plaid;
public sealed class PlaidAccountMetadata : AccountMetadata
{
public const string TypeValue = Providers.Plaid;
public string PlaidId { get; init; } = string.Empty;
public string PlaidName { get; init; } = string.Empty;
private PlaidAccountMetadata() : base(TypeValue)
{
}
public static PlaidAccountMetadata From(string plaidId, string plaidName)
{
return new()
{
PlaidId = plaidId,
PlaidName = plaidName,
};
}
}
@@ -1,32 +0,0 @@
namespace FiscalOS.Infra.Accounts.Plaid;
public sealed record PlaidClientOptions
{
public string ClientId { get; init; } = string.Empty;
public string Secret { get; init; } = string.Empty;
public IOptions<PlaidOptions> ToPlaidOptions()
{
return Options.Create<PlaidOptions>(new()
{
ClientId = ClientId,
Secret = Secret
});
}
}
public sealed record PlaidClientOptionsSetup : IConfigureOptions<PlaidClientOptions>
{
private const string SectionName = nameof(PlaidClientOptions);
private readonly IConfiguration _configuration;
public PlaidClientOptionsSetup(IConfiguration configuration)
{
_configuration = configuration;
}
public void Configure(PlaidClientOptions options)
{
_configuration.GetSection(SectionName).Bind(options);
}
}
@@ -1,16 +0,0 @@
namespace FiscalOS.Infra.Accounts.Plaid;
public class PlaidException : Exception
{
public PlaidException()
{
}
public PlaidException(string message) : base(message)
{
}
public PlaidException(string message, Exception innerException) : base(message, innerException)
{
}
}
@@ -1,15 +0,0 @@
namespace FiscalOS.Infra.Accounts.Plaid;
public static class PlaidExtensions
{
#pragma warning disable CA1034
extension(Going.Plaid.Entity.Account act)
{
public string CurrencyCode => act.Balances.IsoCurrencyCode
?? act.Balances.UnofficialCurrencyCode
?? "Unknown";
public decimal Available => act.Balances.Available.GetValueOrDefault();
public decimal Current => act.Balances.Current.GetValueOrDefault();
}
}
@@ -1,31 +0,0 @@
namespace FiscalOS.Infra.Accounts.Plaid;
public sealed class PlaidMetadata : InstitutionMetadata
{
public const string TypeValue = Providers.Plaid;
public string PlaidId { get; init; } = string.Empty;
public string PlaidName { get; init; } = string.Empty;
public string EncryptedAccessToken { get; init; } = string.Empty;
private PlaidMetadata() : base(TypeValue)
{
}
public static PlaidMetadata From(
string? plaidId,
string? plaidName,
string encryptedAccessToken
)
{
ArgumentNullException.ThrowIfNull(plaidId, nameof(plaidId));
ArgumentNullException.ThrowIfNull(plaidName, nameof(plaidName));
ArgumentNullException.ThrowIfNull(encryptedAccessToken, nameof(encryptedAccessToken));
return new PlaidMetadata
{
PlaidId = plaidId,
PlaidName = plaidName,
EncryptedAccessToken = encryptedAccessToken
};
}
}
@@ -1,91 +0,0 @@
using System.Reflection;
namespace FiscalOS.Infra.Accounts.Plaid;
public sealed class PlaidService
{
private readonly PlaidClient _client;
private PlaidService(PlaidClient client)
{
_client = client;
}
public static PlaidService From(IServiceProvider sp)
{
var client = sp.GetRequiredService<PlaidClient>();
return new(client);
}
public async Task<string> CreateLinkTokenAsync(string id)
{
var assemblyName = Assembly.GetExecutingAssembly().GetName().FullName;
var environmentName = System.Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var ltr = await _client.LinkTokenCreateAsync(new()
{
ClientName = $"{assemblyName}_{environmentName}",
Products = [Products.Transactions],
CountryCodes = [CountryCode.Us],
Language = Language.English,
User = new()
{
ClientUserId = id,
},
}).ConfigureAwait(false);
if (ltr.IsSuccessStatusCode is false)
{
throw new PlaidException("Unable to create link token");
}
return ltr.LinkToken;
}
public async Task<(string ItemId, string AccessToken)> ExchangeTokenAsync(string publicToken)
{
var ptr = await _client.ItemPublicTokenExchangeAsync(new()
{
PublicToken = publicToken,
}).ConfigureAwait(false);
if (ptr.IsSuccessStatusCode is false)
{
throw new PlaidException("Unable to exchange public token for access token");
}
return (ptr.ItemId, ptr.AccessToken);
}
public async Task<List<Going.Plaid.Entity.Account>> GetAccountsAsync(string accessToken)
{
var ar = await _client.AccountsGetAsync(new()
{
AccessToken = accessToken,
}).ConfigureAwait(false);
if (ar.IsSuccessStatusCode is false)
{
throw new PlaidException("Unable to retrieve accounts");
}
return [.. ar.Accounts];
}
public async Task<ItemWithConsentFields> GetItemAsync(string accessToken)
{
var ar = await _client.ItemGetAsync(new()
{
AccessToken = accessToken,
}).ConfigureAwait(false);
if (ar.IsSuccessStatusCode is false)
{
throw new PlaidException("Unable to retrieve item");
}
return ar.Item;
}
}
-6
View File
@@ -1,6 +0,0 @@
namespace FiscalOS.Infra.Accounts;
public static class Providers
{
public const string Plaid = nameof(Plaid);
}
@@ -6,8 +6,6 @@ public sealed record JwtOptions
public string Audience { get; init; } = string.Empty;
public string Secret { get; init; } = string.Empty;
public int ExpiryInMinutes { get; init; } = 5;
public SymmetricSecurityKey Key => new(Encoding.UTF8.GetBytes(Secret));
}
public sealed record JwtOptionsSetup : IConfigureOptions<JwtOptions>
@@ -24,33 +22,4 @@ public sealed record JwtOptionsSetup : IConfigureOptions<JwtOptions>
{
_configuration.GetSection(SectionName).Bind(options);
}
}
public sealed class JwtBearerOptionsSetup(IOptions<JwtOptions> jwtOptions) : IConfigureNamedOptions<JwtBearerOptions>
{
private readonly JwtOptions _jwtOptions = jwtOptions.Value;
public void Configure(string? name, JwtBearerOptions options)
{
options.TokenValidationParameters = new()
{
ValidIssuer = _jwtOptions.Issuer,
ValidAudience = _jwtOptions.Audience,
IssuerSigningKey = _jwtOptions.Key,
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
};
if (name is Schemes.AllowExpiredTokens)
{
options.TokenValidationParameters.ValidateLifetime = false;
}
}
public void Configure(JwtBearerOptions options)
{
Configure(Options.DefaultName, options);
}
}
@@ -1,7 +0,0 @@
namespace FiscalOS.Infra.Authentication;
public static class Schemes
{
public const string Default = JwtBearerDefaults.AuthenticationScheme;
public const string AllowExpiredTokens = "AllowExpiredTokens";
}
@@ -33,6 +33,7 @@ public sealed class TokenGenerator : ITokenGenerator
public string GenerateAccessToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var secretKeyBytes = Encoding.UTF8.GetBytes(_jwtOptions.Value.Secret);
var issuedAt = _timeProvider.GetUtcNow();
var expiresAt = issuedAt.AddMinutes(_jwtOptions.Value.ExpiryInMinutes);
List<Claim> claims = [
@@ -48,7 +49,7 @@ public sealed class TokenGenerator : ITokenGenerator
Issuer = _jwtOptions.Value.Issuer,
Audience = _jwtOptions.Value.Audience,
SigningCredentials = new(
_jwtOptions.Value.Key,
new SymmetricSecurityKey(secretKeyBytes),
SecurityAlgorithms.HmacSha256Signature
),
};
@@ -1,41 +0,0 @@
namespace FiscalOS.Infra.Authorization;
internal sealed class ProblemDetailsAuthResultHandler : IAuthorizationMiddlewareResultHandler
{
public Task HandleAsync(
RequestDelegate next,
HttpContext context,
AuthorizationPolicy policy,
PolicyAuthorizationResult authorizeResult
)
{
if (authorizeResult.Succeeded)
{
return next(context);
}
context.Response.StatusCode = authorizeResult.Forbidden
? StatusCodes.Status403Forbidden
: StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/problem+json";
var problemDetails = authorizeResult.Forbidden
? new ProblemDetails
{
Title = "Forbidden",
Status = StatusCodes.Status403Forbidden,
Detail = "You do not have permission to access this resource."
}
: new ProblemDetails
{
Title = "Unauthorized",
Status = StatusCodes.Status401Unauthorized,
Detail = "Authentication is required to access this resource."
};
var json = JsonSerializer.Serialize(problemDetails);
return context.Response.WriteAsync(json);
}
}
+8 -99
View File
@@ -1,28 +1,21 @@
using Account = FiscalOS.Core.Accounts.Account;
using Institution = FiscalOS.Core.Accounts.Institution;
namespace FiscalOS.Infra.Data;
public sealed class AppDbContext(
IOptions<AppDbContextOptions> ctxOptions,
IFileSystem fileSystem
) : DbContext
public sealed class AppDbContext(IOptions<AppDbContextOptions> ctxOptions) : DbContext
{
private const string DataSourceKey = "Data Source=";
private readonly AppDbContextOptions _ctxOptions = ctxOptions.Value;
private readonly IFileSystem _fileSystem = fileSystem;
public DbSet<User> Users => Set<User>();
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var dbPath = _fileSystem.Path.GetFullPath(_ctxOptions.DatabaseFilePath, AppContext.BaseDirectory);
var dbDirectory = _fileSystem.Path.GetDirectoryName(dbPath) ?? throw new InvalidOperationException("Database directory path could not be determined.");
var dbPath = _ctxOptions.GetFullyQualifiedDatabasePath();
var dbDirectory = Path.GetDirectoryName(dbPath) ?? throw new InvalidOperationException("Database directory path could not be determined.");
if (_fileSystem.Directory.Exists(dbDirectory) is false)
if (Directory.Exists(dbDirectory) is false)
{
_fileSystem.Directory.CreateDirectory(dbDirectory);
Directory.CreateDirectory(dbDirectory);
}
var connectionString = $"{DataSourceKey}{dbPath}";
@@ -52,108 +45,24 @@ public sealed class AppDbContext(
modelBuilder.Entity<User>(static eb =>
{
eb.Property(static u => u.Username);
eb.HasIndex(static u => u.Username).IsUnique();
eb.Property(static u => u.HashedPassword);
eb.HasMany(static u => u.RefreshTokens)
.WithOne(static t => t.User)
.HasForeignKey(static t => t.UserId)
.OnDelete(DeleteBehavior.Cascade);
eb.HasMany(static u => u.Institutions)
.WithOne()
.HasForeignKey(static i => i.UserId)
.OnDelete(DeleteBehavior.Cascade);
eb.Property(static u => u.Username);
eb.HasIndex(static u => u.Username).IsUnique();
eb.HasMany(static u => u.Accounts)
.WithOne()
.HasForeignKey(static a => a.UserId)
.OnDelete(DeleteBehavior.Cascade);
eb.Property(static u => u.HashedPassword);
});
modelBuilder.Entity<RefreshToken>(static eb =>
{
eb.Property(static t => t.Id);
eb.Property(static t => t.ExpiresAt);
eb.Property(static t => t.UserId);
eb.Property(static t => t.Token);
eb.HasIndex(static t => t.Token).IsUnique();
});
modelBuilder.Entity<Institution>(static eb =>
{
eb.Property(static i => i.Name);
eb.HasOne(static i => i.Metadata)
.WithOne()
.HasForeignKey<InstitutionMetadata>(static m => m.InstitutionId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<InstitutionMetadata>(static eb =>
{
eb.HasDiscriminator(static m => m.Type)
.HasValue<PlaidMetadata>(PlaidMetadata.TypeValue);
eb.Property(static m => m.InstitutionId);
eb.Property(static m => m.Type);
});
modelBuilder.Entity<PlaidMetadata>(static eb =>
{
eb.HasBaseType<InstitutionMetadata>();
eb.Property(static m => m.PlaidId);
eb.Property(static m => m.PlaidName);
eb.Property(static m => m.EncryptedAccessToken);
});
modelBuilder.Entity<Account>(static eb =>
{
eb.Property(static a => a.Name);
eb.HasOne(static a => a.Institution)
.WithMany()
.HasForeignKey(static a => a.InstitutionId)
.OnDelete(DeleteBehavior.Cascade);
eb.HasOne(static a => a.Metadata)
.WithOne()
.HasForeignKey<AccountMetadata>(static m => m.AccountId)
.OnDelete(DeleteBehavior.Cascade);
eb.HasMany(static a => a.Balances)
.WithOne()
.HasForeignKey(static b => b.AccountId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<AccountMetadata>(static eb =>
{
eb.HasDiscriminator(static m => m.Type)
.HasValue<PlaidAccountMetadata>(PlaidAccountMetadata.TypeValue);
eb.Property(static m => m.AccountId);
eb.Property(static m => m.Type);
});
modelBuilder.Entity<PlaidAccountMetadata>(static eb =>
{
eb.HasBaseType<AccountMetadata>();
eb.Property(static m => m.PlaidId);
eb.Property(static m => m.PlaidName);
});
modelBuilder.Entity<Balance>(static eb =>
{
eb.Property(static b => b.AccountId);
eb.Property(static b => b.Current);
eb.Property(static b => b.Available);
eb.Property(static b => b.CurrencyCode);
});
}
}
@@ -3,6 +3,11 @@ namespace FiscalOS.Infra.Data;
public sealed record AppDbContextOptions
{
public string DatabaseFilePath { get; init; } = string.Empty;
public string GetFullyQualifiedDatabasePath()
{
return Path.GetFullPath(DatabaseFilePath, AppContext.BaseDirectory);
}
}
public sealed record AppDbContextOptionsSetup : IConfigureOptions<AppDbContextOptions>
@@ -1,3 +1,5 @@
using FiscalOS.Core.Data;
namespace FiscalOS.Infra.Data;
internal sealed class TimestampInterceptor : SaveChangesInterceptor
@@ -4,37 +4,13 @@ public static class ServiceCollectionExtensions
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{
services.AddSingleton<IAuthorizationMiddlewareResultHandler, ProblemDetailsAuthResultHandler>();
services.AddHttpClient();
services.AddSingleton(TimeProvider.System);
services.AddSingleton<IFileSystem, FileSystem>();
services.ConfigureOptions<JwtOptionsSetup>();
services.ConfigureOptions<JwtBearerOptionsSetup>();
services.AddSingleton<ITokenGenerator>(TokenGenerator.From);
services.AddSingleton<IPasswordHasher>(PasswordHasher.From);
services.ConfigureOptions<FileKeyRingOptionsSetup>();
services.AddSingleton<IKeyRing>(FileKeyRing.From);
services.AddSingleton<IEncryptor>(Encryptor.From);
services.AddSingleton<ITokenGenerator, TokenGenerator>(TokenGenerator.From);
services.AddSingleton<IPasswordHasher, PasswordHasher>(PasswordHasher.From);
services.ConfigureOptions<AppDbContextOptionsSetup>();
services.AddDbContext<AppDbContext>();
services.AddHostedService<MigrationService>();
services.ConfigureOptions<PlaidClientOptionsSetup>();
services.AddSingleton(static sp =>
{
var factory = sp.GetRequiredService<IHttpClientFactory>();
var logger = sp.GetRequiredService<ILogger<PlaidClient>>();
var clientOptions = sp.GetRequiredService<IOptions<PlaidClientOptions>>();
var options = clientOptions.Value.ToPlaidOptions();
return new PlaidClient(options, factory, logger);
});
services.AddSingleton(PlaidService.From);
return services;
}
}
-2
View File
@@ -1,11 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Going.Plaid" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" />
</ItemGroup>
<ItemGroup>
@@ -1,4 +1,6 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@@ -1,4 +1,6 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@@ -1,4 +1,6 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@@ -1,4 +1,6 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
@@ -1,101 +0,0 @@
// <auto-generated />
using System;
using FiscalOS.Infra.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260203115122_AddUserIdPropToRefreshToken")]
partial class AddUserIdPropToRefreshToken
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Token")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("HashedPassword")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", "User")
.WithMany("RefreshTokens")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Navigation("RefreshTokens");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,22 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
/// <inheritdoc />
public partial class AddUserIdPropToRefreshToken : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
@@ -1,104 +0,0 @@
// <auto-generated />
using System;
using FiscalOS.Infra.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260203164938_AddRevokedPropertyToRefreshToken")]
partial class AddRevokedPropertyToRefreshToken
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<bool>("Revoked")
.HasColumnType("INTEGER");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Token")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("HashedPassword")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", "User")
.WithMany("RefreshTokens")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Navigation("RefreshTokens");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
/// <inheritdoc />
public partial class AddRevokedPropertyToRefreshToken : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Revoked",
table: "RefreshTokens",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Revoked",
table: "RefreshTokens");
}
}
}
@@ -1,108 +0,0 @@
// <auto-generated />
using System;
using FiscalOS.Infra.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260205192220_AddEncryptionKeyToUserModel")]
partial class AddEncryptionKeyToUserModel
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<bool>("Revoked")
.HasColumnType("INTEGER");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Token")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("EncryptedDataKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("HashedPassword")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", "User")
.WithMany("RefreshTokens")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Navigation("RefreshTokens");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
/// <inheritdoc />
public partial class AddEncryptionKeyToUserModel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "EncryptedDataKey",
table: "Users",
type: "TEXT",
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "EncryptedDataKey",
table: "Users");
}
}
}
@@ -1,112 +0,0 @@
// <auto-generated />
using System;
using FiscalOS.Infra.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260205222656_AddEncryptionKeyIdToUserModel")]
partial class AddEncryptionKeyIdToUserModel
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<bool>("Revoked")
.HasColumnType("INTEGER");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Token")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("EncryptedDataKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("EncryptionKeyId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("HashedPassword")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", "User")
.WithMany("RefreshTokens")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Navigation("RefreshTokens");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
/// <inheritdoc />
public partial class AddEncryptionKeyIdToUserModel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "EncryptionKeyId",
table: "Users",
type: "TEXT",
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "EncryptionKeyId",
table: "Users");
}
}
}
@@ -1,215 +0,0 @@
// <auto-generated />
using System;
using FiscalOS.Infra.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260212215015_InstitutionAndMetadata")]
partial class InstitutionAndMetadata
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Institution");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("InstitutionId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("InstitutionId")
.IsUnique();
b.ToTable("InstitutionMetadata");
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<bool>("Revoked")
.HasColumnType("INTEGER");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Token")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("EncryptedDataKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("EncryptionKeyId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("HashedPassword")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
{
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
b.Property<string>("EncryptedAccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidName")
.IsRequired()
.HasColumnType("TEXT");
b.HasDiscriminator().HasValue("Plaid");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", null)
.WithMany("Institutions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
.WithOne("Metadata")
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", "User")
.WithMany("RefreshTokens")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Navigation("Metadata")
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Navigation("Institutions");
b.Navigation("RefreshTokens");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,82 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
/// <inheritdoc />
public partial class InstitutionAndMetadata : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Institution",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Institution", x => x.Id);
table.ForeignKey(
name: "FK_Institution_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "InstitutionMetadata",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
Type = table.Column<string>(type: "TEXT", maxLength: 21, nullable: false),
InstitutionId = table.Column<Guid>(type: "TEXT", nullable: false),
PlaidId = table.Column<string>(type: "TEXT", nullable: true),
PlaidName = table.Column<string>(type: "TEXT", nullable: true),
EncryptedAccessToken = table.Column<string>(type: "TEXT", nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_InstitutionMetadata", x => x.Id);
table.ForeignKey(
name: "FK_InstitutionMetadata_Institution_InstitutionId",
column: x => x.InstitutionId,
principalTable: "Institution",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Institution_UserId",
table: "Institution",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_InstitutionMetadata_InstitutionId",
table: "InstitutionMetadata",
column: "InstitutionId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "InstitutionMetadata");
migrationBuilder.DropTable(
name: "Institution");
}
}
}
@@ -1,327 +0,0 @@
// <auto-generated />
using System;
using FiscalOS.Infra.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260213130218_AccountAndMetadata")]
partial class AccountAndMetadata
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("InstituionId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("InstituionId");
b.HasIndex("UserId");
b.ToTable("Account");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("AccountId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccountId")
.IsUnique();
b.ToTable("AccountMetadata");
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Institution");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("InstitutionId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("InstitutionId")
.IsUnique();
b.ToTable("InstitutionMetadata");
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<bool>("Revoked")
.HasColumnType("INTEGER");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Token")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("EncryptedDataKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("EncryptionKeyId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("HashedPassword")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
{
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
b.Property<string>("PlaidId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidName")
.IsRequired()
.HasColumnType("TEXT");
b.HasDiscriminator().HasValue("Plaid");
});
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
{
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
b.Property<string>("EncryptedAccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidName")
.IsRequired()
.HasColumnType("TEXT");
b.HasDiscriminator().HasValue("Plaid");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
.WithMany()
.HasForeignKey("InstituionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FiscalOS.Core.Identity.User", null)
.WithMany("Accounts")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Institution");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Account", null)
.WithOne("Metadata")
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", null)
.WithMany("Institutions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
.WithOne("Metadata")
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", "User")
.WithMany("RefreshTokens")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.Navigation("Metadata")
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Navigation("Metadata")
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Navigation("Accounts");
b.Navigation("Institutions");
b.Navigation("RefreshTokens");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,93 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
/// <inheritdoc />
public partial class AccountAndMetadata : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Account",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
InstituionId = table.Column<Guid>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Account", x => x.Id);
table.ForeignKey(
name: "FK_Account_Institution_InstituionId",
column: x => x.InstituionId,
principalTable: "Institution",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Account_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AccountMetadata",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
Type = table.Column<string>(type: "TEXT", maxLength: 21, nullable: false),
AccountId = table.Column<Guid>(type: "TEXT", nullable: false),
PlaidId = table.Column<string>(type: "TEXT", nullable: true),
PlaidName = table.Column<string>(type: "TEXT", nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AccountMetadata", x => x.Id);
table.ForeignKey(
name: "FK_AccountMetadata_Account_AccountId",
column: x => x.AccountId,
principalTable: "Account",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Account_InstituionId",
table: "Account",
column: "InstituionId");
migrationBuilder.CreateIndex(
name: "IX_Account_UserId",
table: "Account",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AccountMetadata_AccountId",
table: "AccountMetadata",
column: "AccountId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AccountMetadata");
migrationBuilder.DropTable(
name: "Account");
}
}
}
@@ -1,327 +0,0 @@
// <auto-generated />
using System;
using FiscalOS.Infra.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260213180002_FixAccountInstitutionColumnName")]
partial class FixAccountInstitutionColumnName
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("InstitutionId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("InstitutionId");
b.HasIndex("UserId");
b.ToTable("Account");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("AccountId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccountId")
.IsUnique();
b.ToTable("AccountMetadata");
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Institution");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("InstitutionId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("InstitutionId")
.IsUnique();
b.ToTable("InstitutionMetadata");
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<bool>("Revoked")
.HasColumnType("INTEGER");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Token")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("EncryptedDataKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("EncryptionKeyId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("HashedPassword")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
{
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
b.Property<string>("PlaidId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidName")
.IsRequired()
.HasColumnType("TEXT");
b.HasDiscriminator().HasValue("Plaid");
});
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
{
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
b.Property<string>("EncryptedAccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidName")
.IsRequired()
.HasColumnType("TEXT");
b.HasDiscriminator().HasValue("Plaid");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
.WithMany()
.HasForeignKey("InstitutionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FiscalOS.Core.Identity.User", null)
.WithMany("Accounts")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Institution");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Account", null)
.WithOne("Metadata")
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", null)
.WithMany("Institutions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
.WithOne("Metadata")
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", "User")
.WithMany("RefreshTokens")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.Navigation("Metadata")
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Navigation("Metadata")
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Navigation("Accounts");
b.Navigation("Institutions");
b.Navigation("RefreshTokens");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,62 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
/// <inheritdoc />
public partial class FixAccountInstitutionColumnName : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Account_Institution_InstituionId",
table: "Account");
migrationBuilder.RenameColumn(
name: "InstituionId",
table: "Account",
newName: "InstitutionId");
migrationBuilder.RenameIndex(
name: "IX_Account_InstituionId",
table: "Account",
newName: "IX_Account_InstitutionId");
migrationBuilder.AddForeignKey(
name: "FK_Account_Institution_InstitutionId",
table: "Account",
column: "InstitutionId",
principalTable: "Institution",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Account_Institution_InstitutionId",
table: "Account");
migrationBuilder.RenameColumn(
name: "InstitutionId",
table: "Account",
newName: "InstituionId");
migrationBuilder.RenameIndex(
name: "IX_Account_InstitutionId",
table: "Account",
newName: "IX_Account_InstituionId");
migrationBuilder.AddForeignKey(
name: "FK_Account_Institution_InstituionId",
table: "Account",
column: "InstituionId",
principalTable: "Institution",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}
@@ -1,364 +0,0 @@
// <auto-generated />
using System;
using FiscalOS.Infra.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260214112234_AddBalancesWithConnectionToAccounts")]
partial class AddBalancesWithConnectionToAccounts
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("InstitutionId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("InstitutionId");
b.HasIndex("UserId");
b.ToTable("Account");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("AccountId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccountId")
.IsUnique();
b.ToTable("AccountMetadata");
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("AccountId")
.HasColumnType("TEXT");
b.Property<decimal>("Available")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<decimal>("Current")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccountId");
b.ToTable("Balance");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Institution");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("InstitutionId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("InstitutionId")
.IsUnique();
b.ToTable("InstitutionMetadata");
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<bool>("Revoked")
.HasColumnType("INTEGER");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Token")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("EncryptedDataKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("EncryptionKeyId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("HashedPassword")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
{
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
b.Property<string>("PlaidId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidName")
.IsRequired()
.HasColumnType("TEXT");
b.HasDiscriminator().HasValue("Plaid");
});
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
{
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
b.Property<string>("EncryptedAccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidName")
.IsRequired()
.HasColumnType("TEXT");
b.HasDiscriminator().HasValue("Plaid");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
.WithMany()
.HasForeignKey("InstitutionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FiscalOS.Core.Identity.User", null)
.WithMany("Accounts")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Institution");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Account", null)
.WithOne("Metadata")
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Account", null)
.WithMany("Balances")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", null)
.WithMany("Institutions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
.WithOne("Metadata")
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", "User")
.WithMany("RefreshTokens")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.Navigation("Balances");
b.Navigation("Metadata");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Navigation("Metadata");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Navigation("Accounts");
b.Navigation("Institutions");
b.Navigation("RefreshTokens");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,50 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
/// <inheritdoc />
public partial class AddBalancesWithConnectionToAccounts : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Balance",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
AccountId = table.Column<Guid>(type: "TEXT", nullable: false),
Current = table.Column<decimal>(type: "TEXT", nullable: false),
Available = table.Column<decimal>(type: "TEXT", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Balance", x => x.Id);
table.ForeignKey(
name: "FK_Balance_Account_AccountId",
column: x => x.AccountId,
principalTable: "Account",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Balance_AccountId",
table: "Balance",
column: "AccountId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Balance");
}
}
}
@@ -1,368 +0,0 @@
// <auto-generated />
using System;
using FiscalOS.Infra.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260214122622_AddCurrencyCodeToBalanceModel")]
partial class AddCurrencyCodeToBalanceModel
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("InstitutionId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("InstitutionId");
b.HasIndex("UserId");
b.ToTable("Account");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("AccountId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccountId")
.IsUnique();
b.ToTable("AccountMetadata");
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("AccountId")
.HasColumnType("TEXT");
b.Property<decimal>("Available")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("CurrencyCode")
.IsRequired()
.HasColumnType("TEXT");
b.Property<decimal>("Current")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccountId");
b.ToTable("Balance");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Institution");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("InstitutionId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("InstitutionId")
.IsUnique();
b.ToTable("InstitutionMetadata");
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<bool>("Revoked")
.HasColumnType("INTEGER");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Token")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("EncryptedDataKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("EncryptionKeyId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("HashedPassword")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
{
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
b.Property<string>("PlaidId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidName")
.IsRequired()
.HasColumnType("TEXT");
b.HasDiscriminator().HasValue("Plaid");
});
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
{
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
b.Property<string>("EncryptedAccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidName")
.IsRequired()
.HasColumnType("TEXT");
b.HasDiscriminator().HasValue("Plaid");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
.WithMany()
.HasForeignKey("InstitutionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FiscalOS.Core.Identity.User", null)
.WithMany("Accounts")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Institution");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Account", null)
.WithOne("Metadata")
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Account", null)
.WithMany("Balances")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", null)
.WithMany("Institutions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
.WithOne("Metadata")
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", "User")
.WithMany("RefreshTokens")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.Navigation("Balances");
b.Navigation("Metadata");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Navigation("Metadata");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Navigation("Accounts");
b.Navigation("Institutions");
b.Navigation("RefreshTokens");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FiscalOS.Infra.Migrations
{
/// <inheritdoc />
public partial class AddCurrencyCodeToBalanceModel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CurrencyCode",
table: "Balance",
type: "TEXT",
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CurrencyCode",
table: "Balance");
}
}
}
@@ -17,159 +17,6 @@ namespace FiscalOS.Infra.Migrations
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("InstitutionId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("InstitutionId");
b.HasIndex("UserId");
b.ToTable("Account");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("AccountId")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccountId")
.IsUnique();
b.ToTable("AccountMetadata");
b.HasDiscriminator<string>("Type").HasValue("AccountMetadata");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("AccountId")
.HasColumnType("TEXT");
b.Property<decimal>("Available")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("CurrencyCode")
.IsRequired()
.HasColumnType("TEXT");
b.Property<decimal>("Current")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccountId");
b.ToTable("Balance");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Institution");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("InstitutionId")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("InstitutionId")
.IsUnique();
b.ToTable("InstitutionMetadata");
b.HasDiscriminator<string>("Type").HasValue("InstitutionMetadata");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.Property<Guid>("Id")
@@ -182,9 +29,6 @@ namespace FiscalOS.Infra.Migrations
b.Property<DateTimeOffset>("ExpiresAt")
.HasColumnType("TEXT");
b.Property<bool>("Revoked")
.HasColumnType("INTEGER");
b.Property<string>("Token")
.IsRequired()
.HasColumnType("TEXT");
@@ -214,14 +58,6 @@ namespace FiscalOS.Infra.Migrations
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("EncryptedDataKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("EncryptionKeyId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("HashedPassword")
.IsRequired()
.HasColumnType("TEXT");
@@ -241,93 +77,6 @@ namespace FiscalOS.Infra.Migrations
b.ToTable("Users");
});
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidAccountMetadata", b =>
{
b.HasBaseType("FiscalOS.Core.Accounts.AccountMetadata");
b.Property<string>("PlaidId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidName")
.IsRequired()
.HasColumnType("TEXT");
b.HasDiscriminator().HasValue("Plaid");
});
modelBuilder.Entity("FiscalOS.Infra.Accounts.Plaid.PlaidMetadata", b =>
{
b.HasBaseType("FiscalOS.Core.Accounts.InstitutionMetadata");
b.Property<string>("EncryptedAccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("PlaidName")
.IsRequired()
.HasColumnType("TEXT");
b.HasDiscriminator().HasValue("Plaid");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Institution", "Institution")
.WithMany()
.HasForeignKey("InstitutionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("FiscalOS.Core.Identity.User", null)
.WithMany("Accounts")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Institution");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.AccountMetadata", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Account", null)
.WithOne("Metadata")
.HasForeignKey("FiscalOS.Core.Accounts.AccountMetadata", "AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Balance", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Account", null)
.WithMany("Balances")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", null)
.WithMany("Institutions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Accounts.InstitutionMetadata", b =>
{
b.HasOne("FiscalOS.Core.Accounts.Institution", null)
.WithOne("Metadata")
.HasForeignKey("FiscalOS.Core.Accounts.InstitutionMetadata", "InstitutionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("FiscalOS.Core.Identity.RefreshToken", b =>
{
b.HasOne("FiscalOS.Core.Identity.User", "User")
@@ -339,24 +88,8 @@ namespace FiscalOS.Infra.Migrations
b.Navigation("User");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Account", b =>
{
b.Navigation("Balances");
b.Navigation("Metadata");
});
modelBuilder.Entity("FiscalOS.Core.Accounts.Institution", b =>
{
b.Navigation("Metadata");
});
modelBuilder.Entity("FiscalOS.Core.Identity.User", b =>
{
b.Navigation("Accounts");
b.Navigation("Institutions");
b.Navigation("RefreshTokens");
});
#pragma warning restore 612, 618
-105
View File
@@ -1,105 +0,0 @@
namespace FiscalOS.Infra.Security;
public sealed class Encryptor : IEncryptor
{
private readonly IKeyRing _keyRing;
private Encryptor(IKeyRing keyRing)
{
_keyRing = keyRing;
}
public static Encryptor From(IServiceProvider serviceProvider)
{
var keyRing = serviceProvider.GetRequiredService<IKeyRing>();
return new Encryptor(keyRing);
}
public static Encryptor From(IKeyRing keyRing)
{
return new Encryptor(keyRing);
}
private KeyRingEntry PrimaryKey => _keyRing.GetPrimaryKey();
private static async Task<string> DecryptCoreAsync(string key, string cipherText, CancellationToken ct)
{
var cipherTextBytes = Convert.FromBase64String(cipherText);
using var aes = Aes.Create();
aes.Key = Convert.FromBase64String(key);
var iv = new byte[16];
Array.Copy(cipherTextBytes, 0, iv, 0, 16);
aes.IV = iv;
var decryptor = aes.CreateDecryptor();
using var memoryStream = new MemoryStream(cipherTextBytes, 16, cipherTextBytes.Length - 16);
using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
using var streamReader = new StreamReader(cryptoStream);
return await streamReader.ReadToEndAsync(ct).ConfigureAwait(false);
}
public async Task<string> DecryptAsync(string cipherText, CancellationToken ct)
{
return await DecryptCoreAsync(PrimaryKey.Key, cipherText, ct).ConfigureAwait(false);
}
public async Task<string> DecryptAsyncFor(User user, string plainText, CancellationToken ct)
{
var decryptedKey = await DecryptCoreAsync(PrimaryKey.Key, user.EncryptedDataKey, ct).ConfigureAwait(false);
return await DecryptCoreAsync(decryptedKey, plainText, ct).ConfigureAwait(false);
}
private static async Task<string> EncryptCoreAsync(string key, string plainText, CancellationToken ct)
{
using var aes = Aes.Create();
aes.Key = Convert.FromBase64String(key);
using var memoryStream = new MemoryStream();
aes.GenerateIV();
await memoryStream.WriteAsync(aes.IV, ct).ConfigureAwait(false);
var encryptor = aes.CreateEncryptor();
using var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
using var streamWriter = new StreamWriter(cryptoStream);
await streamWriter.WriteAsync(plainText.ToCharArray(), ct).ConfigureAwait(false);
await streamWriter.FlushAsync(ct).ConfigureAwait(false);
streamWriter.Close();
return Convert.ToBase64String(memoryStream.ToArray());
}
public async Task<string> EncryptAsync(string plainText, CancellationToken ct)
{
return await EncryptCoreAsync(PrimaryKey.Key, plainText, ct).ConfigureAwait(false);
}
public async Task<string> EncryptAsyncFor(User user, string plainText, CancellationToken ct)
{
var decryptedKey = await DecryptCoreAsync(PrimaryKey.Key, user.EncryptedDataKey, ct).ConfigureAwait(false);
return await EncryptCoreAsync(decryptedKey, plainText, ct).ConfigureAwait(false);
}
public string GenerateKey()
{
using var aes = Aes.Create();
aes.GenerateKey();
return Convert.ToBase64String(aes.Key);
}
public async Task<EncryptedDataKey> GenerateEncryptedKeyAsync(CancellationToken ct)
{
var key = GenerateKey();
var keyUsed = PrimaryKey;
var encryptedKey = await EncryptCoreAsync(keyUsed.Key, key, ct).ConfigureAwait(false);
return EncryptedDataKey.From(keyUsed.KeyId, encryptedKey);
}
}
@@ -1,90 +0,0 @@
namespace FiscalOS.Infra.Security;
public sealed class FileKeyRing : IKeyRing, IDisposable
{
private const string KeyFileExtension = ".key";
private FileKeyRingOptions _options;
private readonly IFileSystem _fileSystem;
private readonly IDisposable? _optionsChangeHandler;
private readonly Dictionary<string, KeyRingEntry> _keys = [];
private string KeyRingPath => _fileSystem.Path.GetFullPath(_options.KeysDirectoryPath, AppContext.BaseDirectory);
private FileKeyRing(IOptionsMonitor<FileKeyRingOptions> options, IFileSystem fileSystem)
{
_fileSystem = fileSystem;
_options = options.CurrentValue;
LoadKeys();
_optionsChangeHandler = options.OnChange(options =>
{
_options = options;
LoadKeys();
});
}
public static FileKeyRing From(IServiceProvider serviceProvider)
{
var options = serviceProvider.GetRequiredService<IOptionsMonitor<FileKeyRingOptions>>();
var fileSystem = serviceProvider.GetRequiredService<IFileSystem>();
return new(options, fileSystem);
}
public static FileKeyRing From(IOptionsMonitor<FileKeyRingOptions> options, IFileSystem fileSystem)
{
return new(options, fileSystem);
}
public KeyRingEntry GetKey(string keyId)
{
if (_keys.TryGetValue(keyId, out var key))
{
return key;
}
throw new KeyNotFoundException($"Key with ID '{keyId}' not found in the key ring.");
}
public KeyRingEntry GetPrimaryKey()
{
return GetKey(_options.PrimaryKeyId);
}
private void LoadKeys()
{
_keys.Clear();
if (_fileSystem.Directory.Exists(KeyRingPath))
{
var keyFiles = _fileSystem.Directory.GetFiles(KeyRingPath, $"*{KeyFileExtension}");
foreach (var keyFile in keyFiles)
{
var keyId = _fileSystem.Path.GetFileNameWithoutExtension(keyFile);
var key = _fileSystem.File.ReadAllText(keyFile).Trim();
if (string.IsNullOrEmpty(key) is false)
{
_keys[keyId] = KeyRingEntry.From(keyId, key);
}
}
}
}
public void Dispose()
{
_optionsChangeHandler?.Dispose();
}
public async Task<KeyRingEntry> SaveKeyAsync(string key)
{
var id = Guid.NewGuid().ToString();
var entry = KeyRingEntry.From(id, key);
var filename = id + KeyFileExtension;
var entryPath = _fileSystem.Path.Combine(KeyRingPath, filename);
await _fileSystem.File.WriteAllTextAsync(entryPath, key).ConfigureAwait(false);
return entry;
}
}
@@ -1,23 +0,0 @@
namespace FiscalOS.Infra.Security;
public sealed record FileKeyRingOptions
{
public string KeysDirectoryPath { get; init; } = string.Empty;
public string PrimaryKeyId { get; init; } = string.Empty;
}
public sealed record class FileKeyRingOptionsSetup : IConfigureOptions<FileKeyRingOptions>
{
private const string SectionName = nameof(FileKeyRingOptions);
private readonly IConfiguration _configuration;
public FileKeyRingOptionsSetup(IConfiguration configuration)
{
_configuration = configuration;
}
public void Configure(FileKeyRingOptions options)
{
_configuration.GetSection(SectionName).Bind(options);
}
}
-15
View File
@@ -1,29 +1,14 @@
global using System.IdentityModel.Tokens.Jwt;
global using System.IO.Abstractions;
global using System.Security.Claims;
global using System.Security.Cryptography;
global using System.Text;
global using System.Text.Json;
global using FiscalOS.Core.Accounts;
global using FiscalOS.Core.Authentication;
global using FiscalOS.Core.Data;
global using FiscalOS.Core.Identity;
global using FiscalOS.Core.Security;
global using FiscalOS.Infra.Accounts.Plaid;
global using FiscalOS.Infra.Authentication;
global using FiscalOS.Infra.Authorization;
global using FiscalOS.Infra.Data;
global using FiscalOS.Infra.Security;
global using Going.Plaid;
global using Going.Plaid.Entity;
global using Microsoft.AspNetCore.Authentication.JwtBearer;
global using Microsoft.AspNetCore.Authorization;
global using Microsoft.AspNetCore.Authorization.Policy;
global using Microsoft.AspNetCore.Http;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.EntityFrameworkCore.Diagnostics;
global using Microsoft.Extensions.Configuration;
@@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
</ItemGroup>
</Project>
@@ -1,89 +0,0 @@
namespace FiscalOS.ServiceDefaults;
public static class ServiceDefaultsExtensions
{
private const string HealthEndpointPath = "/health";
private const string AlivenessEndpointPath = "/alive";
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(static http =>
{
http.AddStandardResilienceHandler();
http.AddServiceDiscovery();
});
return builder;
}
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
)
.WithTracing(tracing => tracing.AddSource(builder.Environment.ApplicationName)
.AddAspNetCoreInstrumentation(tracing =>
tracing.Filter = context =>
context.Request.Path.StartsWithSegments(HealthEndpointPath, StringComparison.InvariantCultureIgnoreCase) is false &&
context.Request.Path.StartsWithSegments(AlivenessEndpointPath, StringComparison.InvariantCultureIgnoreCase) is false
)
.AddHttpClientInstrumentation()
);
builder.AddOpenTelemetryExporters();
return builder;
}
public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.Services.AddHealthChecks()
.AddCheck("self", static () => HealthCheckResult.Healthy(), ["live"]);
return builder;
}
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
ArgumentNullException.ThrowIfNull(app);
if (app.Environment.IsDevelopment())
{
app.MapHealthChecks(HealthEndpointPath);
app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
{
Predicate = static r => r.Tags.Contains("live")
});
}
return app;
}
private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
if (useOtlpExporter)
{
builder.Services.AddOpenTelemetry().UseOtlpExporter();
}
return builder;
}
}
-10
View File
@@ -1,10 +0,0 @@
global using Microsoft.AspNetCore.Builder;
global using Microsoft.AspNetCore.Diagnostics.HealthChecks;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Diagnostics.HealthChecks;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
global using OpenTelemetry;
global using OpenTelemetry.Metrics;
global using OpenTelemetry.Trace;
-8
View File
@@ -1,8 +0,0 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100
-1
View File
@@ -1 +0,0 @@
* text=auto eol=lf
-42
View File
@@ -1,42 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/
# Vite
*.timestamp-*-*.mjs
test-results/
playwright-report/

Some files were not shown because too many files have changed in this diff Show More