Skip to content

Commit

Permalink
feat(boilerplate): apply small refactoring in Boilerplate #8604 (#8605)
Browse files Browse the repository at this point in the history
  • Loading branch information
ysmoradi authored Sep 13, 2024
1 parent c04dbe4 commit fcd8b76
Show file tree
Hide file tree
Showing 18 changed files with 129 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,10 @@ jobs:
displayName: 'Build'
inputs:
targetType: 'inline'
script: 'dotnet build Boilerplate.sln -c Release'
script: 'dotnet build Boilerplate.sln -c Release'

- task: Bash@3
displayName: 'Test'
inputs:
targetType: 'inline'
script: 'dotnet test src/Tests/Boilerplate.Tests.csproj'
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ jobs:
node-version: 20

- name: Build
run: dotnet build Boilerplate.sln -c Release
run: dotnet build Boilerplate.sln -c Release

- name: Test
run: dotnet test src/Tests/Boilerplate.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ private async Task ConfirmEmail()

await WrapRequest(async () =>
{
await identityController.ConfirmEmail(new() { Email = emailModel.Email, Token = emailModel.Token }, CurrentCancellationToken);
var signInResponse = await identityController.ConfirmEmail(new() { Email = emailModel.Email, Token = emailModel.Token }, CurrentCancellationToken);

await AuthenticationManager.OnNewToken(signInResponse, true);

NavigationManager.NavigateTo(Urls.HomePage, replace: true);

isEmailConfirmed = true;
});
Expand All @@ -101,7 +105,11 @@ private async Task ConfirmPhone()

await WrapRequest(async () =>
{
await identityController.ConfirmPhone(new() { PhoneNumber = phoneModel.PhoneNumber, Token = phoneModel.Token }, CurrentCancellationToken);
var signInResponse = await identityController.ConfirmPhone(new() { PhoneNumber = phoneModel.PhoneNumber, Token = phoneModel.Token }, CurrentCancellationToken);

await AuthenticationManager.OnNewToken(signInResponse, true);

NavigationManager.NavigateTo(Urls.HomePage, replace: true);

isPhoneConfirmed = true;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ private async Task DoSignIn()

if (requiresTwoFactor is false)
{
NavigationManager.NavigateTo(ReturnUrlQueryString ?? "/", replace: true);
NavigationManager.NavigateTo(ReturnUrlQueryString ?? Urls.HomePage, replace: true);
}
}
catch (KnownException e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,18 @@ public async Task<bool> SignIn(SignInRequestDto request, CancellationToken cance

if (response.RequiresTwoFactor) return true;

await StoreTokens(response, request.RememberMe);
await OnNewToken(response!, request.RememberMe);

return false;
}

public async Task OnNewToken(TokenResponseDto response, bool? rememberMe = null)
{
await StoreTokens(response, rememberMe);

var state = await GetAuthenticationStateAsync();

NotifyAuthenticationStateChanged(Task.FromResult(state));

return false;
}

public async Task SignOut(CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class AuthDelegatingHandler(IAuthTokenProvider tokenProvider, IServicePro
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.Headers.Authorization is null && HasNoAuthHeaderPolicy(request) is false)
if (request.Headers.Authorization is null)
{
var access_token = await tokenProvider.GetAccessTokenAsync();
if (access_token is not null)
Expand Down Expand Up @@ -47,17 +47,4 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
return await base.SendAsync(request, cancellationToken);
}
}

/// <summary>
/// <see cref="NoAuthorizeHeaderPolicyAttribute"/>
/// </summary>
private static bool HasNoAuthHeaderPolicy(HttpRequestMessage request)
{
if (request.Options.TryGetValue(new(RequestOptionNames.IControllerType), out Type? controllerType) is false)
return false;

var parameterTypes = ((Dictionary<string, Type>)request.Options.GetValueOrDefault(RequestOptionNames.ActionParametersInfo)!).Select(p => p.Value).ToArray();
var method = controllerType!.GetMethod((string)request.Options.GetValueOrDefault(RequestOptionNames.ActionName)!, parameterTypes)!;
return method.GetCustomAttribute<NoAuthorizeHeaderPolicyAttribute>() is not null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,16 @@ public class CustomWKNavigationDelegate : WKNavigationDelegate
{
public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, WKWebpagePreferences preferences, Action<WKNavigationActionPolicy, WKWebpagePreferences> decisionHandler)
{
// To open Google reCAPTCHA and similar elements directly within the webview.
decisionHandler?.Invoke(WKNavigationActionPolicy.Allow, preferences);

if (navigationAction.NavigationType is WKNavigationType.LinkActivated)
{
// https://developer.apple.com/documentation/webkit/wknavigationtype/linkactivated#discussion
_ = Browser.OpenAsync(navigationAction.Request.Url!);
decisionHandler.Invoke(WKNavigationActionPolicy.Cancel, preferences);
}
else
{
// To open Google reCAPTCHA and similar elements directly within the webview.
decisionHandler.Invoke(WKNavigationActionPolicy.Allow, preferences);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ protected override void OnCreate(Bundle? savedInstanceState)
var url = Intent?.DataString;
if (string.IsNullOrWhiteSpace(url) is false)
{
_ = Routes.OpenUniversalLink(new URL(url).File ?? "/");
_ = Routes.OpenUniversalLink(new URL(url).File ?? Urls.HomePage);
}
}

Expand All @@ -50,7 +50,7 @@ protected override void OnNewIntent(Intent? intent)
var url = intent.DataString;
if (action is Intent.ActionView && string.IsNullOrWhiteSpace(url) is false)
{
_ = Routes.OpenUniversalLink(new URL(url).File ?? "/");
_ = Routes.OpenUniversalLink(new URL(url).File ?? Urls.HomePage);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,20 @@ public class IdentitySettings : IdentityOptions
/// <summary>
/// To either confirm and/or change email
/// </summary>
public TimeSpan EmailTokenRequestResendDelay { get; set; }
public TimeSpan EmailTokenLifetime { get; set; }
/// <summary>
/// To either confirm and/or change phone number
/// </summary>
public TimeSpan PhoneNumberTokenRequestResendDelay { get; set; }
public TimeSpan ResetPasswordTokenRequestResendDelay { get; set; }
public TimeSpan TwoFactorTokenRequestResendDelay { get; set; }
public TimeSpan RevokeUserSessionsDelay { get; set; }
public TimeSpan PhoneNumberTokenLifetime { get; set; }
public TimeSpan ResetPasswordTokenLifetime { get; set; }
public TimeSpan TwoFactorTokenLifetime { get; set; }

/// <summary>
/// To sign in with either Otp or magic link.
/// </summary>
public TimeSpan OtpRequestResendDelay { get; set; }
public TimeSpan OtpTokenLifetime { get; set; }

public TimeSpan RevokeUserSessionsDelay { get; set; }
}

public class EmailSettings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
<PackageReference Include="Microsoft.AspNetCore.OData" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8" PrivateAssets="all" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.8" />
<PackageReference Condition=" '$(database)' == 'SqlServer' OR '$(database)' == '' " Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
<PackageReference Condition=" '$(database)' == 'Sqlite' OR '$(database)' == '' " Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
<PackageReference Condition=" '$(database)' == 'PostgreSQL' OR '$(database)' == '' " Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
Expand Down Expand Up @@ -70,11 +69,6 @@
</ItemGroup>

<ItemGroup>
<Compile Update="**\*.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>$([System.String]::Copy('%(Filename)').Replace('.Designer','')).resx</DependentUpon>
</Compile>
<EmbeddedResource Update="Resources\EmailStrings.resx">
<Generator>MSBuild:Compile</Generator>
<LastGenOutput>Resources\EmailStrings.Designer.cs</LastGenOutput>
Expand All @@ -86,7 +80,7 @@
</EmbeddedResource>
</ItemGroup>

<ItemGroup>
<ItemGroup Condition=" '$(api)' == 'Integrated' OR '$(api)' == ''">
<Content Update="appsettings*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,15 @@ public async Task SendConfirmEmailToken(SendEmailTokenRequestDto request, Cancel
}

[HttpPost]
public async Task ConfirmEmail(ConfirmEmailRequestDto request, CancellationToken cancellationToken)
public async Task<ActionResult<SignInResponseDto>> ConfirmEmail(ConfirmEmailRequestDto request, CancellationToken cancellationToken)
{
var user = await userManager.FindByEmailAsync(request.Email!)
?? throw new BadRequestException(Localizer[nameof(AppStrings.UserNotFound)]);

if (await userManager.IsEmailConfirmedAsync(user)) return;
var expired = (DateTimeOffset.Now - user.EmailTokenRequestedOn) > AppSettings.Identity.EmailTokenLifetime;

if (expired)
throw new BadRequestException();

if (await userManager.IsLockedOutAsync(user))
throw new BadRequestException(Localizer[nameof(AppStrings.UserLockedOut), (DateTimeOffset.UtcNow - user.LockoutEnd!).Value.Humanize(culture: CultureInfo.CurrentUICulture)]);
Expand All @@ -127,6 +130,10 @@ public async Task ConfirmEmail(ConfirmEmailRequestDto request, CancellationToken
var updateResult = await userManager.UpdateAsync(user);
if (updateResult.Succeeded is false)
throw new ResourceValidationException(updateResult.Errors.Select(e => new LocalizedString(e.Code, e.Description)).ToArray());

var token = await userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, FormattableString.Invariant($"Otp,{user.OtpRequestedOn?.ToUniversalTime()}"));

return await SignIn(new() { Email = request.Email, Otp = token }, cancellationToken);
}

[HttpPost]
Expand All @@ -142,16 +149,19 @@ public async Task SendConfirmPhoneToken(SendPhoneTokenRequestDto request, Cancel
}

[HttpPost]
public async Task ConfirmPhone(ConfirmPhoneRequestDto request, CancellationToken cancellationToken)
public async Task<ActionResult<SignInResponseDto>> ConfirmPhone(ConfirmPhoneRequestDto request, CancellationToken cancellationToken)
{
var user = await userManager.FindByPhoneNumber(request.PhoneNumber!)
?? throw new BadRequestException(Localizer[nameof(AppStrings.UserNotFound)]);

var expired = (DateTimeOffset.Now - user.PhoneNumberTokenRequestedOn) > AppSettings.Identity.PhoneNumberTokenLifetime;

if (expired)
throw new BadRequestException();

if (await userManager.IsLockedOutAsync(user))
throw new BadRequestException(Localizer[nameof(AppStrings.UserLockedOut), (DateTimeOffset.UtcNow - user.LockoutEnd!).Value.Humanize(culture: CultureInfo.CurrentUICulture)]);

if (await userManager.IsPhoneNumberConfirmedAsync(user)) return;

var tokenIsValid = await userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, FormattableString.Invariant($"VerifyPhoneNumber:{request.PhoneNumber},{user.PhoneNumberTokenRequestedOn?.ToUniversalTime()}"), request.Token!);

if (tokenIsValid is false)
Expand All @@ -169,6 +179,10 @@ public async Task ConfirmPhone(ConfirmPhoneRequestDto request, CancellationToken
var updateResult = await userManager.UpdateAsync(user);
if (updateResult.Succeeded is false)
throw new ResourceValidationException(updateResult.Errors.Select(e => new LocalizedString(e.Code, e.Description)).ToArray());

var token = await userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, FormattableString.Invariant($"Otp,{user.OtpRequestedOn?.ToUniversalTime()}"));

return await SignIn(new() { PhoneNumber = request.PhoneNumber, Otp = token }, cancellationToken);
}

[HttpPost]
Expand All @@ -180,7 +194,17 @@ public async Task<ActionResult<SignInResponseDto>> SignIn(SignInRequestDto reque

var userSession = CreateUserSession(request.DeviceInfo);

var result = string.IsNullOrEmpty(request.Otp) is false
bool isOtpSignIn = string.IsNullOrEmpty(request.Otp) is false;

if (isOtpSignIn)
{
var expired = (DateTimeOffset.Now - user.OtpRequestedOn) > AppSettings.Identity.OtpTokenLifetime;

if (expired)
throw new BadRequestException();
}

var result = isOtpSignIn
? await signInManager.OtpSignInAsync(user, request.Otp!)
: await signInManager.PasswordSignInAsync(user!.UserName!, request.Password!, isPersistent: false, lockoutOnFailure: true);

Expand Down Expand Up @@ -321,7 +345,7 @@ public async Task SendResetPasswordToken(SendResetPasswordTokenRequestDto reques
if (await userConfirmation.IsConfirmedAsync(userManager, user) is false)
throw new BadRequestException(Localizer[nameof(AppStrings.UserIsNotConfirmed)]);

var resendDelay = (DateTimeOffset.Now - user.ResetPasswordTokenRequestedOn) - AppSettings.Identity.ResetPasswordTokenRequestResendDelay;
var resendDelay = (DateTimeOffset.Now - user.ResetPasswordTokenRequestedOn) - AppSettings.Identity.ResetPasswordTokenLifetime;

if (resendDelay < TimeSpan.Zero)
throw new TooManyRequestsExceptions(Localizer[nameof(AppStrings.WaitForResetPasswordTokenRequestResendDelay), resendDelay.Value.Humanize(culture: CultureInfo.CurrentUICulture)]);
Expand Down Expand Up @@ -368,7 +392,7 @@ public async Task SendOtp(IdentityRequestDto request, string? returnUrl = null,
if (await userConfirmation.IsConfirmedAsync(userManager, user) is false)
throw new BadRequestException(Localizer[nameof(AppStrings.UserIsNotConfirmed)]);

var resendDelay = (DateTimeOffset.Now - user.OtpRequestedOn) - AppSettings.Identity.OtpRequestResendDelay;
var resendDelay = (DateTimeOffset.Now - user.OtpRequestedOn) - AppSettings.Identity.OtpTokenLifetime;

if (resendDelay < TimeSpan.Zero)
throw new TooManyRequestsExceptions(Localizer[nameof(AppStrings.WaitForOtpRequestResendDelay), resendDelay.Value.Humanize(culture: CultureInfo.CurrentUICulture)]);
Expand Down Expand Up @@ -399,6 +423,11 @@ public async Task ResetPassword(ResetPasswordRequestDto request, CancellationTok
{
var user = await userManager.FindUserAsync(request) ?? throw new ResourceNotFoundException(Localizer[nameof(AppStrings.UserNotFound)]);

var expired = (DateTimeOffset.Now - user.ResetPasswordTokenRequestedOn) > AppSettings.Identity.ResetPasswordTokenLifetime;

if (expired)
throw new BadRequestException();

if (await userManager.IsLockedOutAsync(user))
throw new BadRequestException(Localizer[nameof(AppStrings.UserLockedOut), (DateTimeOffset.UtcNow - user.LockoutEnd!).Value.Humanize(culture: CultureInfo.CurrentUICulture)]);

Expand Down Expand Up @@ -427,7 +456,7 @@ public async Task SendTwoFactorToken(IdentityRequestDto request, CancellationTok
{
var user = await userManager.FindUserAsync(request) ?? throw new ResourceNotFoundException(Localizer[nameof(AppStrings.UserNotFound)]);

var resendDelay = (DateTimeOffset.Now - user.TwoFactorTokenRequestedOn) - AppSettings.Identity.TwoFactorTokenRequestResendDelay;
var resendDelay = (DateTimeOffset.Now - user.TwoFactorTokenRequestedOn) - AppSettings.Identity.TwoFactorTokenLifetime;

if (resendDelay < TimeSpan.Zero)
throw new TooManyRequestsExceptions(Localizer[nameof(AppStrings.WaitForTwoFactorTokenRequestResendDelay), resendDelay.Value.Humanize(culture: CultureInfo.CurrentUICulture)]);
Expand Down Expand Up @@ -587,7 +616,7 @@ public async Task<ActionResult> SocialSignedIn()

private async Task SendConfirmEmailToken(User user, CancellationToken cancellationToken)
{
var resendDelay = (DateTimeOffset.Now - user.EmailTokenRequestedOn) - AppSettings.Identity.EmailTokenRequestResendDelay;
var resendDelay = (DateTimeOffset.Now - user.EmailTokenRequestedOn) - AppSettings.Identity.EmailTokenLifetime;

if (resendDelay < TimeSpan.Zero)
throw new TooManyRequestsExceptions(Localizer[nameof(AppStrings.WaitForEmailTokenRequestResendDelay), resendDelay.Value.Humanize(culture: CultureInfo.CurrentUICulture)]);
Expand All @@ -607,7 +636,7 @@ private async Task SendConfirmEmailToken(User user, CancellationToken cancellati

private async Task SendConfirmPhoneToken(User user, CancellationToken cancellationToken)
{
var resendDelay = (DateTimeOffset.Now - user.PhoneNumberTokenRequestedOn) - AppSettings.Identity.PhoneNumberTokenRequestResendDelay;
var resendDelay = (DateTimeOffset.Now - user.PhoneNumberTokenRequestedOn) - AppSettings.Identity.PhoneNumberTokenLifetime;

if (resendDelay < TimeSpan.Zero)
throw new TooManyRequestsExceptions(Localizer[nameof(AppStrings.WaitForPhoneNumberTokenRequestResendDelay), resendDelay.Value.Humanize(culture: CultureInfo.CurrentUICulture)]);
Expand Down
Loading

0 comments on commit fcd8b76

Please sign in to comment.