Merge pull request #26 from lanedirt/25-add-versioning-to-webapi-project

Add versioning to webapi project
This commit is contained in:
Leendert de Borst
2024-06-18 11:45:52 -07:00
committed by GitHub
11 changed files with 55 additions and 41 deletions

View File

@@ -120,5 +120,8 @@ dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
end_of_line = crlf
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_coalesce_expression = false:suggestion
dotnet_style_null_propagation = false:suggestion
# IDE0046: Convert to conditional expression
dotnet_diagnostic.IDE0046.severity = silent

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
@@ -18,6 +18,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>

View File

@@ -10,6 +10,7 @@ namespace AliasVault.Api.Controllers;
using System.Globalization;
using AliasDb;
using AliasVault.Shared.Models.WebApi;
using Asp.Versioning;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@@ -21,6 +22,7 @@ using Service = AliasVault.Shared.Models.WebApi.Service;
/// </summary>
/// <param name="context">DbContext instance.</param>
/// <param name="userManager">UserManager instance.</param>
[ApiVersion("1")]
public class AliasController(AliasDbContext context, UserManager<IdentityUser> userManager) : AuthenticatedRequestController(userManager)
{
/// <summary>

View File

@@ -15,6 +15,7 @@ using System.Security.Cryptography;
using System.Text;
using AliasDb;
using AliasVault.Shared.Models;
using Asp.Versioning;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
@@ -26,8 +27,9 @@ using Microsoft.IdentityModel.Tokens;
/// <param name="userManager">UserManager instance.</param>
/// <param name="signInManager">SignInManager instance.</param>
/// <param name="configuration">IConfiguration instance.</param>
[Route("api/[controller]")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1")]
public class AuthController(AliasDbContext context, UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, IConfiguration configuration) : ControllerBase
{
/// <summary>
@@ -175,7 +177,7 @@ public class AuthController(AliasDbContext context, UserManager<IdentityUser> us
issuer: configuration["Jwt:Issuer"] ?? string.Empty,
audience: configuration["Jwt:Issuer"] ?? string.Empty,
claims: claims,
expires: DateTime.Now.AddMinutes(30),
expires: DateTime.Now.AddSeconds(15),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);

View File

@@ -15,7 +15,7 @@ using Microsoft.AspNetCore.Mvc;
/// Base controller for requests that require authentication.
/// </summary>
/// <param name="userManager">UserManager instance.</param>
[Route("api/[controller]")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[Authorize]
public class AuthenticatedRequestController(UserManager<IdentityUser> userManager) : ControllerBase

View File

@@ -8,6 +8,7 @@ namespace AliasVault.Api.Controllers;
using AliasGenerators.Identity;
using AliasGenerators.Identity.Implementations;
using Asp.Versioning;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@@ -15,6 +16,7 @@ using Microsoft.AspNetCore.Mvc;
/// Controller for identity generation.
/// </summary>
/// <param name="userManager">UserManager instance.</param>
[ApiVersion("1")]
public class IdentityController(UserManager<IdentityUser> userManager) : AuthenticatedRequestController(userManager)
{
/// <summary>

View File

@@ -8,6 +8,7 @@
using System.Data.Common;
using System.Text;
using AliasDb;
using Asp.Versioning;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.Data.Sqlite;
@@ -26,7 +27,6 @@ builder.Services.AddLogging(logging =>
logging.AddFilter("Microsoft.AspNetCore.Identity.UserManager", LogLevel.Error);
});
// Add services to the container.
builder.Services.AddSingleton<DbConnection>(container =>
{
var configFile = new ConfigurationBuilder()
@@ -99,7 +99,19 @@ builder.Services.AddCors(options =>
});
builder.Services.AddControllers();
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "AliasVault API", Version = "v1" });

View File

@@ -1,7 +1,6 @@
@page "/user/login"
@attribute [AllowAnonymous]
@layout Auth.Layout.MainLayout
@inject HttpClient Http
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavigationManager
@@ -71,7 +70,7 @@
try
{
var result = await Http.PostAsJsonAsync("api/Auth/login", _loginModel);
var result = await Http.PostAsJsonAsync("api/v1/Auth/login", _loginModel);
var responseContent = await result.Content.ReadAsStringAsync();
if (!result.IsSuccessStatusCode)

View File

@@ -63,7 +63,7 @@
try
{
var result = await Http.PostAsJsonAsync("api/Auth/register", _registerModel);
var result = await Http.PostAsJsonAsync("api/v1/Auth/register", _registerModel);
var responseContent = await result.Content.ReadAsStringAsync();
if (!result.IsSuccessStatusCode)

View File

@@ -16,23 +16,15 @@ using Blazored.LocalStorage;
/// This service is responsible for handling authentication-related operations such as refreshing tokens,
/// storing tokens, and revoking tokens.
/// </summary>
public class AuthService
/// <remarks>
/// Initializes a new instance of the <see cref="AuthService"/> class.
/// </remarks>
/// <param name="httpClient">The HTTP client.</param>
/// <param name="localStorage">The local storage service.</param>
public class AuthService(HttpClient httpClient, ILocalStorageService localStorage)
{
private const string AccessTokenKey = "token";
private const string RefreshTokenKey = "refreshToken";
private readonly HttpClient _httpClient;
private readonly ILocalStorageService _localStorage;
/// <summary>
/// Initializes a new instance of the <see cref="AuthService"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client.</param>
/// <param name="localStorage">The local storage service.</param>
public AuthService(HttpClient httpClient, ILocalStorageService localStorage)
{
_httpClient = httpClient;
_localStorage = localStorage;
}
/// <summary>
/// Refreshes the access token asynchronously.
@@ -44,14 +36,14 @@ public class AuthService
var accessToken = await GetAccessTokenAsync();
var refreshToken = await GetRefreshTokenAsync();
var tokenInput = new TokenModel { Token = accessToken, RefreshToken = refreshToken };
using var request = new HttpRequestMessage(HttpMethod.Post, "api/Auth/refresh")
using var request = new HttpRequestMessage(HttpMethod.Post, "api/v1/Auth/refresh")
{
Content = JsonContent.Create(tokenInput),
};
// Add the X-Ignore-Failure header to the request so any failure does not trigger another refresh token request.
request.Headers.Add("X-Ignore-Failure", "true");
var response = await _httpClient.SendAsync(request);
var response = await httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
@@ -77,7 +69,7 @@ public class AuthService
/// <returns>The stored access token.</returns>
public async Task<string> GetAccessTokenAsync()
{
return await _localStorage.GetItemAsStringAsync(AccessTokenKey) ?? string.Empty;
return await localStorage.GetItemAsStringAsync(AccessTokenKey) ?? string.Empty;
}
/// <summary>
@@ -87,7 +79,7 @@ public class AuthService
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task StoreAccessTokenAsync(string newToken)
{
await _localStorage.SetItemAsStringAsync(AccessTokenKey, newToken);
await localStorage.SetItemAsStringAsync(AccessTokenKey, newToken);
}
/// <summary>
@@ -96,7 +88,7 @@ public class AuthService
/// <returns>The stored refresh token.</returns>
public async Task<string> GetRefreshTokenAsync()
{
return await _localStorage.GetItemAsStringAsync(RefreshTokenKey) ?? string.Empty;
return await localStorage.GetItemAsStringAsync(RefreshTokenKey) ?? string.Empty;
}
/// <summary>
@@ -106,7 +98,7 @@ public class AuthService
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task StoreRefreshTokenAsync(string newToken)
{
await _localStorage.SetItemAsStringAsync(RefreshTokenKey, newToken);
await localStorage.SetItemAsStringAsync(RefreshTokenKey, newToken);
}
/// <summary>
@@ -115,8 +107,8 @@ public class AuthService
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task RemoveTokensAsync()
{
await _localStorage.RemoveItemAsync(AccessTokenKey);
await _localStorage.RemoveItemAsync(RefreshTokenKey);
await localStorage.RemoveItemAsync(AccessTokenKey);
await localStorage.RemoveItemAsync(RefreshTokenKey);
// If the remote call fails we catch the exception and ignore it.
// This is because the user is already logged out and we don't want to trigger another refresh token request.
@@ -142,13 +134,13 @@ public class AuthService
RefreshToken = await GetRefreshTokenAsync(),
};
using var request = new HttpRequestMessage(HttpMethod.Post, "api/Auth/revoke")
using var request = new HttpRequestMessage(HttpMethod.Post, "api/v1/Auth/revoke")
{
Content = JsonContent.Create(tokenInput),
};
// Add the X-Ignore-Failure header to the request so any failure does not trigger another refresh token request.
request.Headers.Add("X-Ignore-Failure", "true");
await _httpClient.SendAsync(request);
await httpClient.SendAsync(request);
}
}

View File

@@ -22,8 +22,8 @@ public class AliasService(HttpClient httpClient)
/// <returns>Identity object.</returns>
public async Task<Identity> GenerateRandomIdentityAsync()
{
var identity = await httpClient.GetFromJsonAsync<Identity>("api/Identity/generate");
if (identity == null)
var identity = await httpClient.GetFromJsonAsync<Identity>("api/v1/Identity/generate");
if (identity is null)
{
throw new InvalidOperationException("Failed to generate random identity.");
}
@@ -40,7 +40,7 @@ public class AliasService(HttpClient httpClient)
{
try
{
var returnObject = await httpClient.PutAsJsonAsync<Alias>("api/Alias", aliasObject);
var returnObject = await httpClient.PutAsJsonAsync<Alias>("api/v1/Alias", aliasObject);
return await returnObject.Content.ReadFromJsonAsync<Guid>();
}
catch
@@ -59,7 +59,7 @@ public class AliasService(HttpClient httpClient)
{
try
{
var returnObject = await httpClient.PostAsJsonAsync<Alias>("api/Alias/" + id, aliasObject);
var returnObject = await httpClient.PostAsJsonAsync<Alias>("api/v1/Alias/" + id, aliasObject);
return await returnObject.Content.ReadFromJsonAsync<Guid>();
}
catch
@@ -77,7 +77,7 @@ public class AliasService(HttpClient httpClient)
{
try
{
return await httpClient.GetFromJsonAsync<Alias>("api/Alias/" + aliasId);
return await httpClient.GetFromJsonAsync<Alias>("api/v1/Alias/" + aliasId);
}
catch
{
@@ -93,7 +93,7 @@ public class AliasService(HttpClient httpClient)
{
try
{
return await httpClient.GetFromJsonAsync<List<AliasListEntry>>("api/Alias/items");
return await httpClient.GetFromJsonAsync<List<AliasListEntry>>("api/v1/Alias/items");
}
catch
{
@@ -111,7 +111,7 @@ public class AliasService(HttpClient httpClient)
// Delete from webapi.
try
{
await httpClient.DeleteAsync("api/Alias/" + id);
await httpClient.DeleteAsync("api/v1/Alias/" + id);
}
catch
{