Skip to content

Commit 786f80b

Browse files
committed
seems to work
1 parent 026f206 commit 786f80b

61 files changed

Lines changed: 1600 additions & 1477 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/articles/bankid.md

Lines changed: 197 additions & 155 deletions
Large diffs are not rendered by default.

samples/Standalone.MvcSample/Standalone.MvcSample.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@
3131
</ItemGroup>
3232

3333
<ItemGroup>
34-
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
34+
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.23.0" />
3535
</ItemGroup>
3636
</Project>

src/ActiveLogin.Authentication.BankId.AspNetCore/ActiveLogin.Authentication.BankId.AspNetCore.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
<ItemGroup>
1616
<FrameworkReference Include="Microsoft.AspNetCore.App" />
17-
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
17+
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.2" />
1818
<PackageReference Include="ActiveLogin.Identity.Swedish" Version="3.0.0" />
1919
</ItemGroup>
2020

src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiApiControllerBase.cs

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
using ActiveLogin.Authentication.BankId.AspNetCore.DataProtection;
1010
using ActiveLogin.Authentication.BankId.AspNetCore.Helpers;
1111
using ActiveLogin.Authentication.BankId.AspNetCore.Models;
12+
using ActiveLogin.Authentication.BankId.Core;
1213
using ActiveLogin.Authentication.BankId.Core.Flow;
14+
using ActiveLogin.Authentication.BankId.Core.Models;
1315
using ActiveLogin.Authentication.BankId.Core.UserMessage;
1416

1517
using Microsoft.AspNetCore.Http;
@@ -18,43 +20,29 @@
1820
namespace ActiveLogin.Authentication.BankId.AspNetCore.Areas.ActiveLogin.Controllers;
1921

2022
[NonController]
21-
public abstract class BankIdUiApiControllerBase : ControllerBase
23+
public abstract class BankIdUiApiControllerBase(
24+
IBankIdFlowService bankIdFlowService,
25+
IBankIdDataStateProtector<BankIdUiOrderRef> orderRefProtector,
26+
IBankIdDataStateProtector<BankIdQrStartState> qrStartStateProtector,
27+
IBankIdUiOptionsCookieManager uiOptionsCookieManager,
28+
29+
IBankIdUserMessage bankIdUserMessage,
30+
IBankIdUserMessageLocalizer bankIdUserMessageLocalizer,
31+
IBankIdDataStateProtector<BankIdUiResult> uiAuthResultProtector,
32+
IStateStorage stateStorage
33+
) : ControllerBase
2234
{
2335
private static JsonSerializerOptions JsonSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
2436

25-
protected readonly IBankIdFlowService BankIdFlowService;
26-
protected readonly IBankIdUiOrderRefProtector OrderRefProtector;
27-
protected readonly IBankIdQrStartStateProtector QrStartStateProtector;
28-
protected readonly IBankIdUiOptionsProtector UiOptionsProtector;
29-
protected readonly IBankIdUiOptionsCookieManager UiOptionsCookieManager;
37+
protected readonly IBankIdFlowService BankIdFlowService = bankIdFlowService;
38+
protected readonly IBankIdDataStateProtector<BankIdUiOrderRef> OrderRefProtector = orderRefProtector;
39+
protected readonly IBankIdDataStateProtector<BankIdQrStartState> QrStartStateProtector = qrStartStateProtector;
40+
protected readonly IStateStorage _stateStorage = stateStorage;
41+
protected readonly IBankIdUiOptionsCookieManager UiOptionsCookieManager = uiOptionsCookieManager;
3042

31-
private readonly IBankIdUserMessage _bankIdUserMessage;
32-
private readonly IBankIdUserMessageLocalizer _bankIdUserMessageLocalizer;
33-
private readonly IBankIdUiResultProtector _uiAuthResultProtector;
34-
35-
protected BankIdUiApiControllerBase(
36-
IBankIdFlowService bankIdFlowService,
37-
IBankIdUiOrderRefProtector orderRefProtector,
38-
IBankIdQrStartStateProtector qrStartStateProtector,
39-
IBankIdUiOptionsProtector uiOptionsProtector,
40-
IBankIdUiOptionsCookieManager uiOptionsCookieManager,
41-
42-
IBankIdUserMessage bankIdUserMessage,
43-
IBankIdUserMessageLocalizer bankIdUserMessageLocalizer,
44-
IBankIdUiResultProtector uiAuthResultProtector
45-
46-
)
47-
{
48-
BankIdFlowService = bankIdFlowService;
49-
OrderRefProtector = orderRefProtector;
50-
QrStartStateProtector = qrStartStateProtector;
51-
UiOptionsProtector = uiOptionsProtector;
52-
UiOptionsCookieManager = uiOptionsCookieManager;
53-
54-
_bankIdUserMessage = bankIdUserMessage;
55-
_bankIdUserMessageLocalizer = bankIdUserMessageLocalizer;
56-
_uiAuthResultProtector = uiAuthResultProtector;
57-
}
43+
private readonly IBankIdUserMessage _bankIdUserMessage = bankIdUserMessage;
44+
private readonly IBankIdUserMessageLocalizer _bankIdUserMessageLocalizer = bankIdUserMessageLocalizer;
45+
private readonly IBankIdDataStateProtector<BankIdUiResult> _uiAuthResultProtector = uiAuthResultProtector;
5846

5947
[ValidateAntiForgeryToken]
6048
[HttpPost(BankIdConstants.Routes.BankIdApiStatusActionName)]
@@ -83,7 +71,7 @@ public async Task<ActionResult> Status(BankIdUiApiStatusRequest request)
8371
return BadRequestJsonResult(new BankIdUiApiErrorResponse(errorStatusMessage));
8472
}
8573

86-
switch(result)
74+
switch (result)
8775
{
8876
case BankIdFlowCollectResultPending pending:
8977
{
@@ -196,4 +184,18 @@ protected BankIdUiOptions ResolveProtectedUiOptions(string protectedUiOptionsOrG
196184
return UiOptionsCookieManager.Retrieve(protectedUiOptionsOrGuid)
197185
?? throw new InvalidOperationException(BankIdConstants.ErrorMessages.InvalidUiOptions);
198186
}
187+
188+
protected async ValueTask<T?> GetState<T>(BankIdUiOptions uiOptions)
189+
where T : BankIdUiState
190+
{
191+
var cookie = Request.Cookies[uiOptions.StateKeyCookieName];
192+
if (cookie == null)
193+
{
194+
return null;
195+
}
196+
197+
var stateKey = new StateKey(cookie);
198+
return await _stateStorage.GetAsync<T>(stateKey);
199+
}
200+
199201
}

src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiAuthApiController.cs

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
using ActiveLogin.Authentication.BankId.AspNetCore.Models;
88
using ActiveLogin.Authentication.BankId.Core.Flow;
99
using ActiveLogin.Authentication.BankId.Core.UserMessage;
10-
10+
using ActiveLogin.Authentication.BankId.Core;
1111
using Microsoft.AspNetCore.Authorization;
1212
using Microsoft.AspNetCore.Mvc;
1313

@@ -18,21 +18,17 @@ namespace ActiveLogin.Authentication.BankId.AspNetCore.Areas.ActiveLogin.Control
1818
[ApiController]
1919
[AllowAnonymous]
2020
[NonController]
21-
public class BankIdUiAuthApiController : BankIdUiApiControllerBase
21+
public class BankIdUiAuthApiController(
22+
IBankIdFlowService bankIdFlowService,
23+
IBankIdDataStateProtector<BankIdUiOrderRef> orderRefProtector,
24+
IBankIdDataStateProtector<Core.Models.BankIdQrStartState> qrStartStateProtector,
25+
IBankIdUserMessage bankIdUserMessage,
26+
IBankIdUserMessageLocalizer bankIdUserMessageLocalizer,
27+
IBankIdDataStateProtector<BankIdUiResult> uiAuthResultProtector,
28+
IBankIdUiOptionsCookieManager uiOptionsCookieManager,
29+
IStateStorage stateStorage
30+
) : BankIdUiApiControllerBase(bankIdFlowService, orderRefProtector, qrStartStateProtector, uiOptionsCookieManager, bankIdUserMessage, bankIdUserMessageLocalizer, uiAuthResultProtector, stateStorage)
2231
{
23-
public BankIdUiAuthApiController(
24-
IBankIdFlowService bankIdFlowService,
25-
IBankIdUiOrderRefProtector orderRefProtector,
26-
IBankIdQrStartStateProtector qrStartStateProtector,
27-
IBankIdUiOptionsProtector uiOptionsProtector,
28-
IBankIdUiOptionsCookieManager uiOptionsCookieManager,
29-
IBankIdUserMessage bankIdUserMessage,
30-
IBankIdUserMessageLocalizer bankIdUserMessageLocalizer,
31-
IBankIdUiResultProtector uiAuthResultProtector)
32-
: base(bankIdFlowService, orderRefProtector, qrStartStateProtector, uiOptionsProtector, uiOptionsCookieManager, bankIdUserMessage, bankIdUserMessageLocalizer, uiAuthResultProtector)
33-
{
34-
}
35-
3632
[ValidateAntiForgeryToken]
3733
[HttpPost(BankIdConstants.Routes.BankIdApiInitializeActionName)]
3834
public async Task<ActionResult<BankIdUiApiInitializeResponse>> Initialize(BankIdUiApiInitializeRequest request)

src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiAuthController.cs

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
using ActiveLogin.Authentication.BankId.AspNetCore.Auth;
12
using ActiveLogin.Authentication.BankId.AspNetCore.Cookies;
2-
using ActiveLogin.Authentication.BankId.AspNetCore.DataProtection;
33
using ActiveLogin.Authentication.BankId.AspNetCore.StateHandling;
4+
using ActiveLogin.Authentication.BankId.Core;
45
using ActiveLogin.Authentication.BankId.Core.UserMessage;
56

67
using Microsoft.AspNetCore.Antiforgery;
@@ -13,22 +14,15 @@ namespace ActiveLogin.Authentication.BankId.AspNetCore.Areas.ActiveLogin.Control
1314
[Area(BankIdConstants.Routes.ActiveLoginAreaName)]
1415
[AllowAnonymous]
1516
[NonController]
16-
public class BankIdUiAuthController : BankIdUiControllerBase
17+
public class BankIdUiAuthController(
18+
IAntiforgery antiforgery,
19+
IStringLocalizer<ActiveLoginResources> localizer,
20+
IBankIdUserMessageLocalizer bankIdUserMessageLocalizer,
21+
IBankIdInvalidStateHandler bankIdInvalidStateHandler,
22+
IBankIdUiOptionsCookieManager uiOptionsCookieManager,
23+
IStateStorage stateStorage
24+
) : BankIdUiControllerBase<BankIdUiAuthState>(antiforgery, localizer, bankIdUserMessageLocalizer, bankIdInvalidStateHandler, uiOptionsCookieManager, stateStorage)
1725
{
18-
public BankIdUiAuthController(
19-
IAntiforgery antiforgery,
20-
IStringLocalizer<ActiveLoginResources> localizer,
21-
IBankIdUserMessageLocalizer bankIdUserMessageLocalizer,
22-
IBankIdUiOptionsProtector uiOptionsProtector,
23-
IBankIdInvalidStateHandler bankIdInvalidStateHandler,
24-
IBankIdUiStateProtector bankIdUiStateProtector,
25-
IBankIdUiOptionsCookieManager uiOptionsCookieManager
26-
)
27-
: base(antiforgery, localizer, bankIdUserMessageLocalizer, uiOptionsProtector, bankIdInvalidStateHandler, bankIdUiStateProtector, uiOptionsCookieManager)
28-
{
29-
30-
}
31-
3226
[HttpGet]
3327
[Route($"/[area]/{BankIdConstants.Routes.BankIdPathName}/{BankIdConstants.Routes.BankIdAuthControllerPath}")]
3428
public Task<ActionResult> Init(string returnUrl, [FromQuery(Name = BankIdConstants.QueryStringParameters.UiOptions)] string uiOptionsGuid)

src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiControllerBase.cs

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using ActiveLogin.Authentication.BankId.AspNetCore.Payment;
88
using ActiveLogin.Authentication.BankId.AspNetCore.Sign;
99
using ActiveLogin.Authentication.BankId.AspNetCore.StateHandling;
10+
using ActiveLogin.Authentication.BankId.Core;
1011
using ActiveLogin.Authentication.BankId.Core.UserMessage;
1112

1213
using Microsoft.AspNetCore.Antiforgery;
@@ -16,32 +17,32 @@
1617
namespace ActiveLogin.Authentication.BankId.AspNetCore.Areas.ActiveLogin.Controllers;
1718

1819
[NonController]
19-
public abstract class BankIdUiControllerBase : Controller
20+
public abstract class BankIdUiControllerBase<T>(
21+
IAntiforgery antiforgery,
22+
IStringLocalizer<ActiveLoginResources> localizer,
23+
IBankIdUserMessageLocalizer bankIdUserMessageLocalizer,
24+
IBankIdInvalidStateHandler bankIdInvalidStateHandler,
25+
IBankIdUiOptionsCookieManager uiOptionsCookieManager,
26+
IStateStorage stateStorage
27+
) : Controller
28+
where T : BankIdUiState
2029
{
21-
private readonly IAntiforgery _antiforgery;
22-
private readonly IStringLocalizer<ActiveLoginResources> _localizer;
23-
private readonly IBankIdUserMessageLocalizer _bankIdUserMessageLocalizer;
24-
private readonly IBankIdUiOptionsProtector _uiOptionsProtector;
25-
private readonly IBankIdInvalidStateHandler _bankIdInvalidStateHandler;
26-
private readonly IBankIdUiStateProtector _bankIdUiStateProtector;
27-
private readonly IBankIdUiOptionsCookieManager _uiOptionsCookieManager;
28-
29-
protected BankIdUiControllerBase(
30-
IAntiforgery antiforgery,
31-
IStringLocalizer<ActiveLoginResources> localizer,
32-
IBankIdUserMessageLocalizer bankIdUserMessageLocalizer,
33-
IBankIdUiOptionsProtector uiOptionsProtector,
34-
IBankIdInvalidStateHandler bankIdInvalidStateHandler,
35-
IBankIdUiStateProtector bankIdUiStateProtector,
36-
IBankIdUiOptionsCookieManager uiOptionsCookieManager)
30+
private readonly IAntiforgery _antiforgery = antiforgery;
31+
private readonly IStringLocalizer<ActiveLoginResources> _localizer = localizer;
32+
private readonly IBankIdUserMessageLocalizer _bankIdUserMessageLocalizer = bankIdUserMessageLocalizer;
33+
private readonly IBankIdInvalidStateHandler _bankIdInvalidStateHandler = bankIdInvalidStateHandler;
34+
private readonly IBankIdUiOptionsCookieManager _uiOptionsCookieManager = uiOptionsCookieManager;
35+
private readonly IStateStorage stateStorage = stateStorage;
36+
37+
protected Task<T?> GetUIState(BankIdUiOptions uiOptions)
3738
{
38-
_antiforgery = antiforgery;
39-
_localizer = localizer;
40-
_bankIdUserMessageLocalizer = bankIdUserMessageLocalizer;
41-
_uiOptionsProtector = uiOptionsProtector;
42-
_bankIdInvalidStateHandler = bankIdInvalidStateHandler;
43-
_bankIdUiStateProtector = bankIdUiStateProtector;
44-
_uiOptionsCookieManager = uiOptionsCookieManager;
39+
var cookie = HttpContext.Request.Cookies[uiOptions.StateKeyCookieName];
40+
if (cookie is null)
41+
{
42+
return Task.FromResult<T?>(default);
43+
}
44+
var stateKey = new StateKey(cookie);
45+
return stateStorage.GetAsync<T>(stateKey);
4546
}
4647

4748
protected async Task<ActionResult> Initialize(string returnUrl, string apiControllerName, string uiOptionsGuid, string viewName)
@@ -63,32 +64,40 @@ protected async Task<ActionResult> Initialize(string returnUrl, string apiContro
6364
return new EmptyResult();
6465
}
6566

66-
var antiforgeryTokens = _antiforgery.GetAndStoreTokens(HttpContext);
67+
var state = await GetUIState(uiOptions);
6768

68-
var protectedState = Request.Cookies[uiOptions.StateCookieName];
69-
if(protectedState == null)
69+
if (state == null)
7070
{
7171
var invalidStateContext = new BankIdInvalidStateContext(uiOptions.CancelReturnUrl);
7272
await _bankIdInvalidStateHandler.HandleAsync(invalidStateContext);
7373

7474
return new EmptyResult();
7575
}
76-
var state = _bankIdUiStateProtector.Unprotect(protectedState);
7776

77+
var antiforgeryTokens = _antiforgery.GetAndStoreTokens(HttpContext);
7878
var viewModel = GetUiViewModel(returnUrl, apiControllerName, uiOptionsGuid, uiOptions, state, antiforgeryTokens);
7979

8080
return View(viewName, viewModel);
8181
}
8282

8383
private bool HasStateCookie(BankIdUiOptions uiOptions)
8484
{
85-
if (string.IsNullOrEmpty(uiOptions.StateCookieName)
86-
|| !HttpContext.Request.Cookies.ContainsKey(uiOptions.StateCookieName))
85+
if (string.IsNullOrEmpty(uiOptions.StateKeyCookieName))
86+
{
87+
return false;
88+
}
89+
90+
if (!HttpContext.Request.Cookies.ContainsKey(uiOptions.StateKeyCookieName))
91+
{
92+
return false;
93+
}
94+
95+
if (string.IsNullOrEmpty(HttpContext.Request.Cookies[uiOptions.StateKeyCookieName]))
8796
{
8897
return false;
8998
}
9099

91-
return !string.IsNullOrEmpty(HttpContext.Request.Cookies[uiOptions.StateCookieName]);
100+
return true;
92101
}
93102

94103
private BankIdUiViewModel GetUiViewModel(string returnUrl, string apiControllerName, string uiOptionsGuid, BankIdUiOptions unprotectedUiOptions, BankIdUiState uiState, AntiforgeryTokenSet antiforgeryTokens)
@@ -127,7 +136,7 @@ private BankIdUiViewModel GetUiViewModel(string returnUrl, string apiControllerN
127136
var localizedCancelButtonText = _localizer["Cancel_Button"];
128137
var localizedQrCodeImageAltText = _localizer["Qr_Code_Image"];
129138

130-
if(uiState is BankIdUiSignState signState)
139+
if (uiState is BankIdUiSignState signState)
131140
{
132141
var uiSignData = new BankIdUiSignData
133142
{

src/ActiveLogin.Authentication.BankId.AspNetCore/Areas/ActiveLogin/Controllers/BankIdUiPaymentApiController.cs

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using ActiveLogin.Authentication.BankId.AspNetCore.Helpers;
77
using ActiveLogin.Authentication.BankId.AspNetCore.Models;
88
using ActiveLogin.Authentication.BankId.AspNetCore.Payment;
9+
using ActiveLogin.Authentication.BankId.Core;
910
using ActiveLogin.Authentication.BankId.Core.Flow;
1011
using ActiveLogin.Authentication.BankId.Core.Models;
1112
using ActiveLogin.Authentication.BankId.Core.UserMessage;
@@ -20,25 +21,17 @@ namespace ActiveLogin.Authentication.BankId.AspNetCore.Areas.ActiveLogin.Control
2021
[ApiController]
2122
[AllowAnonymous]
2223
[NonController]
23-
public class BankIdUiPaymentApiController : BankIdUiApiControllerBase
24+
public class BankIdUiPaymentApiController(
25+
IBankIdFlowService bankIdFlowService,
26+
IBankIdDataStateProtector<BankIdUiOrderRef> orderRefProtector,
27+
IBankIdDataStateProtector<BankIdQrStartState> qrStartStateProtector,
28+
IBankIdUserMessage bankIdUserMessage,
29+
IBankIdUserMessageLocalizer bankIdUserMessageLocalizer,
30+
IBankIdDataStateProtector<BankIdUiResult> uiAuthResultProtector,
31+
IBankIdUiOptionsCookieManager uiOptionsCookieManager,
32+
IStateStorage stateStorage
33+
) : BankIdUiApiControllerBase(bankIdFlowService, orderRefProtector, qrStartStateProtector, uiOptionsCookieManager, bankIdUserMessage, bankIdUserMessageLocalizer, uiAuthResultProtector, stateStorage)
2434
{
25-
private readonly IBankIdUiStateProtector _bankIdUiStateProtector;
26-
27-
public BankIdUiPaymentApiController(
28-
IBankIdFlowService bankIdFlowService,
29-
IBankIdUiOrderRefProtector orderRefProtector,
30-
IBankIdQrStartStateProtector qrStartStateProtector,
31-
IBankIdUiOptionsProtector uiOptionsProtector,
32-
IBankIdUiOptionsCookieManager uiOptionsCookieManager,
33-
IBankIdUserMessage bankIdUserMessage,
34-
IBankIdUserMessageLocalizer bankIdUserMessageLocalizer,
35-
IBankIdUiResultProtector uiAuthResultProtector,
36-
IBankIdUiStateProtector bankIdUiStateProtector)
37-
: base(bankIdFlowService, orderRefProtector, qrStartStateProtector, uiOptionsProtector, uiOptionsCookieManager, bankIdUserMessage, bankIdUserMessageLocalizer, uiAuthResultProtector)
38-
{
39-
_bankIdUiStateProtector = bankIdUiStateProtector;
40-
}
41-
4235
[ValidateAntiForgeryToken]
4336
[HttpPost(BankIdConstants.Routes.BankIdApiInitializeActionName)]
4437
public async Task<ActionResult<BankIdUiApiInitializeResponse>> Initialize(BankIdUiApiInitializeRequest request)
@@ -48,12 +41,7 @@ public async Task<ActionResult<BankIdUiApiInitializeResponse>> Initialize(BankId
4841

4942
var uiOptions = ResolveProtectedUiOptions(request.UiOptions);
5043

51-
var state = GetStateFromCookie(uiOptions);
52-
if(state == null)
53-
{
54-
throw new InvalidOperationException(BankIdConstants.ErrorMessages.InvalidStateCookie);
55-
}
56-
44+
var state = await GetState<BankIdUiPaymentState>(uiOptions) ?? throw new InvalidOperationException(BankIdConstants.ErrorMessages.InvalidStateCookie);
5745
BankIdFlowInitializeResult bankIdFlowInitializeResult;
5846
try
5947
{
@@ -110,15 +98,4 @@ public async Task<ActionResult<BankIdUiApiInitializeResponse>> Initialize(BankId
11098
}
11199
}
112100
}
113-
114-
private BankIdUiPaymentState? GetStateFromCookie(BankIdUiOptions uiOptions)
115-
{
116-
var protectedState = Request.Cookies[uiOptions.StateCookieName];
117-
if (protectedState == null)
118-
{
119-
throw new InvalidOperationException(BankIdConstants.ErrorMessages.InvalidStateCookie);
120-
}
121-
122-
return _bankIdUiStateProtector.Unprotect(protectedState) as BankIdUiPaymentState;
123-
}
124101
}

0 commit comments

Comments
 (0)