Compare commits
114
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b45463e56b | ||
|
|
fd2cd68d73 | ||
|
|
b4fbf16869 | ||
|
|
d9130e75a4 | ||
|
|
e2537d77c4 | ||
|
|
fcbc9e78c2 | ||
|
|
9042cd4ead | ||
|
|
1c3de212ff | ||
|
|
72a534a8e0 | ||
|
|
74e55b2f48 | ||
|
|
a3dc58d19b | ||
|
|
5a878bf4b3 | ||
|
|
dee331fb0d | ||
|
|
df859b91b0 | ||
|
|
d77e5d45f6 | ||
|
|
2cca5c15a0 | ||
|
|
ab88656c1e | ||
|
|
6b2b19cb5b | ||
|
|
9c0f642f90 | ||
|
|
701b89d704 | ||
|
|
49e4288eba | ||
|
|
497dedf6ac | ||
|
|
29d8758ede | ||
|
|
6190470fe2 | ||
|
|
ed7e25961c | ||
|
|
e331030bb0 | ||
|
|
3b7830b1bc | ||
|
|
dad2ceb450 | ||
|
|
32cf48bf36 | ||
|
|
502f6f61c1 | ||
|
|
6ca90ed25a | ||
|
|
5192c06627 | ||
|
|
2fd55a55cd | ||
|
|
a2e9fc3f30 | ||
|
|
a83dbbcc54 | ||
|
|
a07fd24ee9 | ||
|
|
2fdb849142 | ||
|
|
1a41c3c0b6 | ||
|
|
c4a8aada6b | ||
|
|
dd08eee5a3 | ||
|
|
7cdf87b860 | ||
|
|
8663daabae | ||
|
|
0b7c33a024 | ||
|
|
823ba4dc5a | ||
|
|
1d661ac409 | ||
|
|
069d019d80 | ||
|
|
3f62a246ee | ||
|
|
5da1f1decf | ||
|
|
1f5e4a319a | ||
|
|
683ca3fac4 | ||
|
|
e88adfd2a7 | ||
|
|
6fc8bd0366 | ||
|
|
ab23593359 | ||
|
|
87a68669cf | ||
|
|
b4744076b7 | ||
|
|
5b1a5f003e | ||
|
|
b2086d8185 | ||
|
|
9b4f0c6b9e | ||
|
|
259daf1e5b | ||
|
|
e02a18df00 | ||
|
|
c1932246f3 | ||
|
|
4eb833d2c8 | ||
|
|
af29df5d19 | ||
|
|
04e0a31b27 | ||
|
|
97c7847a89 | ||
|
|
f34fb5ce41 | ||
|
|
38c311bdd1 | ||
|
|
e9b1bd11bb | ||
|
|
bbe3779a53 | ||
|
|
47692fc946 | ||
|
|
453aa3264d | ||
|
|
21d23be2ef | ||
|
|
73598111b2 | ||
|
|
1b89056258 | ||
|
|
f12788fa37 | ||
|
|
57c7a3a701 | ||
|
|
92b3c8e9c4 | ||
|
|
e12fa0111a | ||
|
|
ce5fd1fd45 | ||
|
|
3fbd9402c3 | ||
|
|
a3264bd72d | ||
|
|
e9f133bf43 | ||
|
|
18bd510e75 | ||
|
|
d9f0585c4c | ||
|
|
fcc1356d69 | ||
|
|
0ce97cdec8 | ||
|
|
98de115259 | ||
|
|
2a1a03ef6c | ||
|
|
a7eafa4966 | ||
|
|
b05cc06dff | ||
|
|
a1b993bd62 | ||
|
|
c4d0bf78ab | ||
|
|
e03cc0403a | ||
|
|
1335f62ec3 | ||
|
|
eb2d9fb2d0 | ||
|
|
36b293aa15 | ||
|
|
38a03f56ca | ||
|
|
c2b681d267 | ||
|
|
91662f5629 | ||
|
|
c79136fb26 | ||
|
|
7e67e02a68 | ||
|
|
e66911810f | ||
|
|
12ae1dc33a | ||
|
|
647b185d40 | ||
|
|
00d7c977e7 | ||
|
|
ccda58be07 | ||
|
|
083b587493 | ||
|
|
c037b895f0 | ||
|
|
28e4692ca1 | ||
|
|
1f84f47f31 | ||
|
|
80b4e775d6 | ||
|
|
463ff05831 | ||
|
|
c5a429683c | ||
|
|
42e590d9e6 |
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"appHostPath": "../src/FiscalOS.AppHost/FiscalOS.AppHost.csproj"
|
||||
}
|
||||
+3
-1
@@ -59,7 +59,7 @@ dotnet_style_prefer_auto_properties = true:suggestion
|
||||
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
|
||||
dotnet_style_prefer_compound_assignment = true:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_return = false:silent
|
||||
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
@@ -81,7 +81,9 @@ dotnet_remove_unnecessary_suppression_exclusions = none
|
||||
|
||||
# analyzer settings
|
||||
dotnet_diagnostic.IDE0058.severity = none
|
||||
dotnet_diagnostic.IDE0100.severity = none
|
||||
dotnet_diagnostic.CA1515.severity = none
|
||||
dotnet_diagnostic.CA1848.severity = none
|
||||
|
||||
# var preferences
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"vitest.explorer",
|
||||
"ms-playwright.playwright",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"oxc.oxc-vscode",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
|
||||
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
|
||||
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
|
||||
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}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"test": {
|
||||
"runner": "Microsoft.Testing.Platform"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,25 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
|
||||
<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.EntityFrameworkCore.Design" Version="10.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageVersion>
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[*.cs]
|
||||
|
||||
dotnet_diagnostic.CA1062.severity = none
|
||||
dotnet_diagnostic.CA2007.severity = none
|
||||
@@ -0,0 +1,16 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace FiscalOS.API.Auth.Login;
|
||||
|
||||
internal static class Endpoint
|
||||
{
|
||||
private const string Route = "/login";
|
||||
|
||||
public static RouteHandlerBuilder MapLoginEndpoint(this RouteGroupBuilder groupBuilder)
|
||||
{
|
||||
return groupBuilder.MapPost(Route, HandleAsync);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleAsync(
|
||||
HttpContext httpContext,
|
||||
[FromBody] Request loginRequest,
|
||||
[FromServices] AppDbContext appDbContext,
|
||||
[FromServices] IPasswordHasher passwordHasher,
|
||||
[FromServices] ITokenGenerator tokenService
|
||||
)
|
||||
{
|
||||
var user = await appDbContext.Users.SingleOrDefaultAsync(u => u.Username == loginRequest.Username);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var isCorrectPassword = passwordHasher.Verify(loginRequest.Password, user.HashedPassword);
|
||||
|
||||
if (isCorrectPassword is false)
|
||||
{
|
||||
return Results.Unauthorized();
|
||||
}
|
||||
|
||||
var accessToken = tokenService.GenerateAccessToken(user);
|
||||
var refreshToken = tokenService.GenerateRefreshToken(user);
|
||||
|
||||
user.AddRefreshToken(refreshToken);
|
||||
|
||||
await appDbContext.SaveChangesAsync();
|
||||
|
||||
httpContext.SetRefreshTokenCookie(refreshToken);
|
||||
|
||||
return Results.Ok(Response.From(accessToken));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace FiscalOS.API.Auth.Login;
|
||||
|
||||
public record Request : IValidatableObject
|
||||
{
|
||||
public string Username { get; init; } = string.Empty;
|
||||
public string Password { get; init; } = string.Empty;
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Username))
|
||||
{
|
||||
var fieldName = nameof(Username);
|
||||
yield return new($"The {fieldName} field is required.", [fieldName]);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Password))
|
||||
{
|
||||
var fieldName = nameof(Password);
|
||||
yield return new($"The {fieldName} field is required.", [fieldName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace FiscalOS.API.Auth.Login;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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,7 +1,16 @@
|
||||
<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>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FiscalOS.Core\FiscalOS.Core.csproj" />
|
||||
<ProjectReference Include="..\FiscalOS.Infra\FiscalOS.Infra.csproj" />
|
||||
<ProjectReference Include="..\FiscalOS.ServiceDefaults\FiscalOS.ServiceDefaults.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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,14 +1,45 @@
|
||||
using FiscalOS.ServiceDefaults;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
builder.Services.AddValidation();
|
||||
builder.Services.AddProblemDetails();
|
||||
|
||||
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.Run();
|
||||
@@ -18,6 +18,6 @@
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
global using System.ComponentModel.DataAnnotations;
|
||||
global using System.Security.Claims;
|
||||
global using System.Text.Json.Serialization;
|
||||
|
||||
global using FiscalOS.API.Login;
|
||||
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.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;
|
||||
|
||||
global using Microsoft.AspNetCore.Mvc;
|
||||
global using Microsoft.EntityFrameworkCore;
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"AppDbContextOptions": {
|
||||
"DatabaseFilePath": "DatabaseFilePath"
|
||||
},
|
||||
"JwtOptions": {
|
||||
"Audience": "Audience",
|
||||
"Issuer": "Issuer",
|
||||
"Secret": "Secret",
|
||||
"ExpiryInMinutes": "ExpiryInMinutes"
|
||||
},
|
||||
"FileKeyRingOptions": {
|
||||
"KeysDirectoryPath": "KeysDirectoryPath",
|
||||
"PrimaryKeyId": "PrimaryKeyId"
|
||||
},
|
||||
"PlaidClientOptions": {
|
||||
"ClientId": "ClientId",
|
||||
"Secret": "Secret"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
[*.cs]
|
||||
|
||||
dotnet_diagnostic.CA1303.severity = none
|
||||
dotnet_diagnostic.CA2007.severity = none
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace FiscalOS.AdminCLI;
|
||||
|
||||
internal sealed class App(
|
||||
IAnsiConsole console,
|
||||
IPasswordHasher passwordHasher,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
IEncryptor encryptor,
|
||||
IKeyRing keyRing
|
||||
) : IHostedService
|
||||
{
|
||||
private readonly IAnsiConsole _console = console;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory = serviceScopeFactory;
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var config = scope.ServiceProvider.GetRequiredService<IConfiguration>();
|
||||
var appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
var selectionPrompt = new SelectionPrompt<string>()
|
||||
.Title("What do you want to do?")
|
||||
.AddChoices(Commands.All);
|
||||
|
||||
var command = await _console.PromptAsync(selectionPrompt, cancellationToken);
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case Commands.CreateUser:
|
||||
var username = await _console.AskAsync<string>("Enter the [green]username[/]:", cancellationToken);
|
||||
var password = _console.Prompt(
|
||||
new TextPrompt<string>("Enter the [green]password[/]:")
|
||||
.PromptStyle("red")
|
||||
.Secret()
|
||||
);
|
||||
|
||||
var hashedPassword = passwordHasher.Hash(password);
|
||||
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(cancellationToken);
|
||||
var user = User.From(username, hashedPassword, userEncryptionKey);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Spectre.Console" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings*.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FiscalOS.Infra\FiscalOS.Infra.csproj" />
|
||||
<ProjectReference Include="..\FiscalOS.Core\FiscalOS.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,15 @@
|
||||
await Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration(
|
||||
static c => c.SetBasePath(AppContext.BaseDirectory)
|
||||
.AddJsonFile("appsettings.json")
|
||||
.AddEnvironmentVariables()
|
||||
)
|
||||
.ConfigureLogging(static c => c.ClearProviders())
|
||||
.ConfigureServices(static (_, services) =>
|
||||
{
|
||||
services.AddSingleton(AnsiConsole.Console);
|
||||
services.AddInfrastructure();
|
||||
services.AddHostedService<App>();
|
||||
})
|
||||
.Build()
|
||||
.StartAsync();
|
||||
@@ -0,0 +1,13 @@
|
||||
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;
|
||||
|
||||
global using Microsoft.Extensions.Configuration;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using Microsoft.Extensions.Hosting;
|
||||
global using Microsoft.Extensions.Logging;
|
||||
|
||||
global using Spectre.Console;
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"AppDbContextOptions": {
|
||||
"DatabaseFilePath": "DatabaseFilePath"
|
||||
},
|
||||
"FileKeyRingOptions": {
|
||||
"KeysDirectoryPath": "KeysDirectoryPath",
|
||||
"PrimaryKeyId": "PrimaryKeyId"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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();
|
||||
@@ -0,0 +1,19 @@
|
||||
<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>
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace FiscalOS.AppHost;
|
||||
|
||||
public static class ProjectNames
|
||||
{
|
||||
public const string API = "api";
|
||||
public const string Web = "web";
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace FiscalOS.Core.Authentication;
|
||||
|
||||
public interface IPasswordHasher
|
||||
{
|
||||
string Hash(string password);
|
||||
bool Verify(string providedPassword, string hashedPassword);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace FiscalOS.Core.Authentication;
|
||||
|
||||
public interface ITokenGenerator
|
||||
{
|
||||
string GenerateAccessToken(User user);
|
||||
RefreshToken GenerateRefreshToken(User user);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace FiscalOS.Core.Data;
|
||||
|
||||
public abstract class Entity
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; private set; }
|
||||
public DateTimeOffset UpdatedAt { get; private set; }
|
||||
|
||||
public void SetCreatedAt(DateTimeOffset createdAt)
|
||||
{
|
||||
CreatedAt = createdAt;
|
||||
}
|
||||
|
||||
public void SetUpdatedAt(DateTimeOffset updatedAt)
|
||||
{
|
||||
UpdatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace FiscalOS.Core.Identity;
|
||||
|
||||
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; }
|
||||
|
||||
private RefreshToken()
|
||||
{
|
||||
}
|
||||
|
||||
public static RefreshToken From(Guid userId, string token, DateTimeOffset expiresAt)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
UserId = userId,
|
||||
Token = token,
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
namespace FiscalOS.Core.Identity;
|
||||
|
||||
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;
|
||||
|
||||
private User()
|
||||
{
|
||||
}
|
||||
|
||||
public static User New()
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
public static User From(string username, string hashedPassword, EncryptedDataKey encryptedDataKey)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace FiscalOS.Core.Security;
|
||||
|
||||
public interface IKeyRing
|
||||
{
|
||||
KeyRingEntry GetKey(string keyId);
|
||||
KeyRingEntry GetPrimaryKey();
|
||||
Task<KeyRingEntry> SaveKeyAsync(string key);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
global using FiscalOS.Core.Accounts;
|
||||
global using FiscalOS.Core.Data;
|
||||
global using FiscalOS.Core.Identity;
|
||||
global using FiscalOS.Core.Security;
|
||||
@@ -0,0 +1,4 @@
|
||||
[*.cs]
|
||||
|
||||
dotnet_diagnostic.CA1062.severity = none
|
||||
dotnet_diagnostic.CA5404.severity = none
|
||||
@@ -0,0 +1,21 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace FiscalOS.Infra.Accounts;
|
||||
|
||||
public static class Providers
|
||||
{
|
||||
public const string Plaid = nameof(Plaid);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
namespace FiscalOS.Infra.Authentication;
|
||||
|
||||
public sealed record JwtOptions
|
||||
{
|
||||
public string Issuer { get; init; } = string.Empty;
|
||||
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>
|
||||
{
|
||||
private const string SectionName = nameof(JwtOptions);
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public JwtOptionsSetup(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public void Configure(JwtOptions options)
|
||||
{
|
||||
_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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
namespace FiscalOS.Infra.Authentication;
|
||||
|
||||
public sealed class PasswordHasher : IPasswordHasher
|
||||
{
|
||||
private const int SaltSize = 16;
|
||||
private const int HashSize = 32;
|
||||
private const int Iterations = 100_000;
|
||||
|
||||
private static readonly HashAlgorithmName HashAlgorithm = HashAlgorithmName.SHA512;
|
||||
|
||||
private PasswordHasher()
|
||||
{
|
||||
}
|
||||
|
||||
public static PasswordHasher New()
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
public static PasswordHasher From(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
public string Hash(string password)
|
||||
{
|
||||
var salt = RandomNumberGenerator.GetBytes(SaltSize);
|
||||
var hash = Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
Iterations,
|
||||
HashAlgorithm,
|
||||
HashSize
|
||||
);
|
||||
|
||||
var hashBytes = new byte[SaltSize + HashSize];
|
||||
Array.Copy(salt, 0, hashBytes, 0, SaltSize);
|
||||
Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);
|
||||
|
||||
return Convert.ToBase64String(hashBytes);
|
||||
}
|
||||
|
||||
public bool Verify(string providedPassword, string hashedPassword)
|
||||
{
|
||||
var hashBytes = Convert.FromBase64String(hashedPassword);
|
||||
|
||||
var salt = new byte[SaltSize];
|
||||
Array.Copy(hashBytes, 0, salt, 0, SaltSize);
|
||||
|
||||
var storedHash = new byte[HashSize];
|
||||
Array.Copy(hashBytes, SaltSize, storedHash, 0, HashSize);
|
||||
|
||||
var computedHash = Rfc2898DeriveBytes.Pbkdf2(
|
||||
providedPassword,
|
||||
salt,
|
||||
Iterations,
|
||||
HashAlgorithm,
|
||||
HashSize
|
||||
);
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(storedHash, computedHash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace FiscalOS.Infra.Authentication;
|
||||
|
||||
public static class Schemes
|
||||
{
|
||||
public const string Default = JwtBearerDefaults.AuthenticationScheme;
|
||||
public const string AllowExpiredTokens = "AllowExpiredTokens";
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
namespace FiscalOS.Infra.Authentication;
|
||||
|
||||
public sealed class TokenGenerator : ITokenGenerator
|
||||
{
|
||||
private const int RefreshTokenExpiryInHours = 12;
|
||||
private readonly IOptions<JwtOptions> _jwtOptions;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private TokenGenerator(
|
||||
TimeProvider timeProvider,
|
||||
IOptions<JwtOptions> jwtOptions
|
||||
)
|
||||
{
|
||||
_timeProvider = timeProvider;
|
||||
_jwtOptions = jwtOptions;
|
||||
}
|
||||
|
||||
public static TokenGenerator From(IServiceProvider serviceProvider)
|
||||
{
|
||||
var timeProvider = serviceProvider.GetRequiredService<TimeProvider>();
|
||||
var jwtOptions = serviceProvider.GetRequiredService<IOptions<JwtOptions>>();
|
||||
return new(timeProvider, jwtOptions);
|
||||
}
|
||||
|
||||
public static TokenGenerator From(
|
||||
TimeProvider timeProvider,
|
||||
IOptions<JwtOptions> jwtOptions
|
||||
)
|
||||
{
|
||||
return new(timeProvider, jwtOptions);
|
||||
}
|
||||
|
||||
public string GenerateAccessToken(User user)
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var issuedAt = _timeProvider.GetUtcNow();
|
||||
var expiresAt = issuedAt.AddMinutes(_jwtOptions.Value.ExpiryInMinutes);
|
||||
List<Claim> claims = [
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||
];
|
||||
|
||||
var descriptor = new SecurityTokenDescriptor()
|
||||
{
|
||||
Subject = new(claims),
|
||||
IssuedAt = issuedAt.UtcDateTime,
|
||||
Expires = expiresAt.UtcDateTime,
|
||||
Issuer = _jwtOptions.Value.Issuer,
|
||||
Audience = _jwtOptions.Value.Audience,
|
||||
SigningCredentials = new(
|
||||
_jwtOptions.Value.Key,
|
||||
SecurityAlgorithms.HmacSha256Signature
|
||||
),
|
||||
};
|
||||
|
||||
var securityToken = tokenHandler.CreateJwtSecurityToken(descriptor);
|
||||
var jwtToken = tokenHandler.WriteToken(securityToken);
|
||||
|
||||
return jwtToken;
|
||||
}
|
||||
|
||||
public RefreshToken GenerateRefreshToken(User user)
|
||||
{
|
||||
var expiresAt = _timeProvider
|
||||
.GetUtcNow()
|
||||
.AddHours(RefreshTokenExpiryInHours);
|
||||
|
||||
return RefreshToken.From(user.Id, GenerateToken(), expiresAt);
|
||||
}
|
||||
|
||||
private static string GenerateToken()
|
||||
{
|
||||
var randomBytes = RandomNumberGenerator.GetBytes(32);
|
||||
var token = Convert.ToBase64String(randomBytes);
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
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
|
||||
{
|
||||
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.");
|
||||
|
||||
if (_fileSystem.Directory.Exists(dbDirectory) is false)
|
||||
{
|
||||
_fileSystem.Directory.CreateDirectory(dbDirectory);
|
||||
}
|
||||
|
||||
var connectionString = $"{DataSourceKey}{dbPath}";
|
||||
|
||||
optionsBuilder.UseSqlite(connectionString)
|
||||
.AddInterceptors(TimestampInterceptor.New());
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
var entityTypes = modelBuilder.Model.GetEntityTypes()
|
||||
.Where(static e => typeof(Entity).IsAssignableFrom(e.ClrType));
|
||||
|
||||
foreach (var entityType in entityTypes)
|
||||
{
|
||||
modelBuilder.Entity(entityType.ClrType)
|
||||
.HasKey(nameof(Entity.Id));
|
||||
|
||||
modelBuilder.Entity(entityType.ClrType)
|
||||
.Property(nameof(Entity.CreatedAt));
|
||||
|
||||
modelBuilder.Entity(entityType.ClrType)
|
||||
.Property(nameof(Entity.UpdatedAt));
|
||||
}
|
||||
|
||||
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.HasMany(static u => u.Accounts)
|
||||
.WithOne()
|
||||
.HasForeignKey(static a => a.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace FiscalOS.Infra.Data;
|
||||
|
||||
public sealed record AppDbContextOptions
|
||||
{
|
||||
public string DatabaseFilePath { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed record AppDbContextOptionsSetup : IConfigureOptions<AppDbContextOptions>
|
||||
{
|
||||
private const string SectionName = nameof(AppDbContextOptions);
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public AppDbContextOptionsSetup(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public void Configure(AppDbContextOptions options)
|
||||
{
|
||||
_configuration.GetSection(SectionName).Bind(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace FiscalOS.Infra.Data;
|
||||
|
||||
internal sealed class MigrationService(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<MigrationService> logger
|
||||
) : IHostedService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider = serviceProvider;
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation("Applying database migrations...");
|
||||
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await context.Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
logger.LogInformation("Database migrations applied.");
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace FiscalOS.Infra.Data;
|
||||
|
||||
internal sealed class TimestampInterceptor : SaveChangesInterceptor
|
||||
{
|
||||
private TimestampInterceptor()
|
||||
{
|
||||
}
|
||||
|
||||
public static TimestampInterceptor New()
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
|
||||
{
|
||||
UpdateEntities(eventData.Context);
|
||||
return base.SavingChanges(eventData, result);
|
||||
}
|
||||
|
||||
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
|
||||
{
|
||||
UpdateEntities(eventData.Context);
|
||||
return base.SavingChangesAsync(eventData, result, cancellationToken);
|
||||
}
|
||||
|
||||
private static void UpdateEntities(DbContext? context)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entries = context.ChangeTracker
|
||||
.Entries<Entity>()
|
||||
.Where(static e => e.State is EntityState.Added or EntityState.Modified);
|
||||
|
||||
var utcNow = DateTimeOffset.UtcNow;
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry.State == EntityState.Added)
|
||||
{
|
||||
entry.Entity.SetCreatedAt(utcNow);
|
||||
}
|
||||
|
||||
entry.Entity.SetUpdatedAt(utcNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace FiscalOS.Infra.DependencyInjection;
|
||||
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<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>
|
||||
<ProjectReference Include="..\FiscalOS.Core\FiscalOS.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// <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("20260201115839_AddUsersAndRefreshTokens")]
|
||||
partial class AddUsersAndRefreshTokens
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||
|
||||
modelBuilder.Entity("FiscalOS.API.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.API.Identity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddUsersAndRefreshTokens : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RefreshTokens",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RefreshTokens", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "RefreshTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// <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("20260201125529_AddUsernameToUsers")]
|
||||
partial class AddUsernameToUsers
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||
|
||||
modelBuilder.Entity("FiscalOS.API.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.API.Identity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.API.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.HasOne("FiscalOS.API.Identity.User", "User")
|
||||
.WithMany("RefreshTokens")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FiscalOS.API.Identity.User", b =>
|
||||
{
|
||||
b.Navigation("RefreshTokens");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddUsernameToUsers : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Username",
|
||||
table: "Users",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "UserId",
|
||||
table: "RefreshTokens",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RefreshTokens_UserId",
|
||||
table: "RefreshTokens",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_RefreshTokens_Users_UserId",
|
||||
table: "RefreshTokens",
|
||||
column: "UserId",
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_RefreshTokens_Users_UserId",
|
||||
table: "RefreshTokens");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_RefreshTokens_UserId",
|
||||
table: "RefreshTokens");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Username",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UserId",
|
||||
table: "RefreshTokens");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// <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("20260202010259_AddTimestamps")]
|
||||
partial class AddTimestamps
|
||||
{
|
||||
/// <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>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
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.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTimestamps : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "CreatedAt",
|
||||
table: "Users",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "HashedPassword",
|
||||
table: "Users",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "UpdatedAt",
|
||||
table: "Users",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "CreatedAt",
|
||||
table: "RefreshTokens",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "UpdatedAt",
|
||||
table: "RefreshTokens",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedAt",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "HashedPassword",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UpdatedAt",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CreatedAt",
|
||||
table: "RefreshTokens");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UpdatedAt",
|
||||
table: "RefreshTokens");
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
// <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("20260202125635_AddPropsToRefreshToken")]
|
||||
partial class AddPropsToRefreshToken
|
||||
{
|
||||
/// <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("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.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPropsToRefreshToken : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "ExpiresAt",
|
||||
table: "RefreshTokens",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Token",
|
||||
table: "RefreshTokens",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ExpiresAt",
|
||||
table: "RefreshTokens");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Token",
|
||||
table: "RefreshTokens");
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
// <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("20260202214513_AddIndexesWithUniquenessToModels")]
|
||||
partial class AddIndexesWithUniquenessToModels
|
||||
{
|
||||
/// <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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FiscalOS.Infra.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddIndexesWithUniquenessToModels : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Users_Username",
|
||||
table: "Users",
|
||||
column: "Username",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RefreshTokens_Token",
|
||||
table: "RefreshTokens",
|
||||
column: "Token",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Users_Username",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_RefreshTokens_Token",
|
||||
table: "RefreshTokens");
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
// <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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
// <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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
// <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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
// <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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
// <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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+327
@@ -0,0 +1,327 @@
|
||||
// <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
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user