diff --git a/.editorconfig b/.editorconfig index e756cdac4..b89f30f83 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/src/AliasVault.Api/AliasVault.Api.csproj b/src/AliasVault.Api/AliasVault.Api.csproj index 2da78e0e2..611d7dac4 100644 --- a/src/AliasVault.Api/AliasVault.Api.csproj +++ b/src/AliasVault.Api/AliasVault.Api.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -18,6 +18,8 @@ + + all diff --git a/src/AliasVault.Api/Controllers/AliasController.cs b/src/AliasVault.Api/Controllers/AliasController.cs index c07e7696a..45a4463f8 100644 --- a/src/AliasVault.Api/Controllers/AliasController.cs +++ b/src/AliasVault.Api/Controllers/AliasController.cs @@ -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; /// /// DbContext instance. /// UserManager instance. +[ApiVersion("1")] public class AliasController(AliasDbContext context, UserManager userManager) : AuthenticatedRequestController(userManager) { /// diff --git a/src/AliasVault.Api/Controllers/AuthController.cs b/src/AliasVault.Api/Controllers/AuthController.cs index 338d81f01..091117fda 100644 --- a/src/AliasVault.Api/Controllers/AuthController.cs +++ b/src/AliasVault.Api/Controllers/AuthController.cs @@ -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; /// UserManager instance. /// SignInManager instance. /// IConfiguration instance. -[Route("api/[controller]")] +[Route("api/v{version:apiVersion}/[controller]")] [ApiController] +[ApiVersion("1")] public class AuthController(AliasDbContext context, UserManager userManager, SignInManager signInManager, IConfiguration configuration) : ControllerBase { /// @@ -175,7 +177,7 @@ public class AuthController(AliasDbContext context, UserManager 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); diff --git a/src/AliasVault.Api/Controllers/AuthenticatedRequestController.cs b/src/AliasVault.Api/Controllers/AuthenticatedRequestController.cs index 45586e241..b18ed5434 100644 --- a/src/AliasVault.Api/Controllers/AuthenticatedRequestController.cs +++ b/src/AliasVault.Api/Controllers/AuthenticatedRequestController.cs @@ -15,7 +15,7 @@ using Microsoft.AspNetCore.Mvc; /// Base controller for requests that require authentication. /// /// UserManager instance. -[Route("api/[controller]")] +[Route("api/v{version:apiVersion}/[controller]")] [ApiController] [Authorize] public class AuthenticatedRequestController(UserManager userManager) : ControllerBase diff --git a/src/AliasVault.Api/Controllers/IdentityController.cs b/src/AliasVault.Api/Controllers/IdentityController.cs index 068fb3f57..f7fa6cf58 100644 --- a/src/AliasVault.Api/Controllers/IdentityController.cs +++ b/src/AliasVault.Api/Controllers/IdentityController.cs @@ -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. /// /// UserManager instance. +[ApiVersion("1")] public class IdentityController(UserManager userManager) : AuthenticatedRequestController(userManager) { /// diff --git a/src/AliasVault.Api/Program.cs b/src/AliasVault.Api/Program.cs index 49b0a5c95..61902523a 100644 --- a/src/AliasVault.Api/Program.cs +++ b/src/AliasVault.Api/Program.cs @@ -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(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" }); diff --git a/src/AliasVault.WebApp/Auth/Pages/Login.razor b/src/AliasVault.WebApp/Auth/Pages/Login.razor index cf867beae..0e5d21a30 100644 --- a/src/AliasVault.WebApp/Auth/Pages/Login.razor +++ b/src/AliasVault.WebApp/Auth/Pages/Login.razor @@ -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) diff --git a/src/AliasVault.WebApp/Auth/Pages/Register.razor b/src/AliasVault.WebApp/Auth/Pages/Register.razor index ff73f3bbb..bc56f00c0 100644 --- a/src/AliasVault.WebApp/Auth/Pages/Register.razor +++ b/src/AliasVault.WebApp/Auth/Pages/Register.razor @@ -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) diff --git a/src/AliasVault.WebApp/Auth/Services/AuthService.cs b/src/AliasVault.WebApp/Auth/Services/AuthService.cs index f4c6a0304..8414cc186 100644 --- a/src/AliasVault.WebApp/Auth/Services/AuthService.cs +++ b/src/AliasVault.WebApp/Auth/Services/AuthService.cs @@ -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. /// -public class AuthService +/// +/// Initializes a new instance of the class. +/// +/// The HTTP client. +/// The local storage service. +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; - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP client. - /// The local storage service. - public AuthService(HttpClient httpClient, ILocalStorageService localStorage) - { - _httpClient = httpClient; - _localStorage = localStorage; - } /// /// 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 /// The stored access token. public async Task GetAccessTokenAsync() { - return await _localStorage.GetItemAsStringAsync(AccessTokenKey) ?? string.Empty; + return await localStorage.GetItemAsStringAsync(AccessTokenKey) ?? string.Empty; } /// @@ -87,7 +79,7 @@ public class AuthService /// A representing the asynchronous operation. public async Task StoreAccessTokenAsync(string newToken) { - await _localStorage.SetItemAsStringAsync(AccessTokenKey, newToken); + await localStorage.SetItemAsStringAsync(AccessTokenKey, newToken); } /// @@ -96,7 +88,7 @@ public class AuthService /// The stored refresh token. public async Task GetRefreshTokenAsync() { - return await _localStorage.GetItemAsStringAsync(RefreshTokenKey) ?? string.Empty; + return await localStorage.GetItemAsStringAsync(RefreshTokenKey) ?? string.Empty; } /// @@ -106,7 +98,7 @@ public class AuthService /// A representing the asynchronous operation. public async Task StoreRefreshTokenAsync(string newToken) { - await _localStorage.SetItemAsStringAsync(RefreshTokenKey, newToken); + await localStorage.SetItemAsStringAsync(RefreshTokenKey, newToken); } /// @@ -115,8 +107,8 @@ public class AuthService /// A representing the asynchronous operation. 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); } } diff --git a/src/AliasVault.WebApp/Services/AliasService.cs b/src/AliasVault.WebApp/Services/AliasService.cs index 0c4cfec85..a8d892202 100644 --- a/src/AliasVault.WebApp/Services/AliasService.cs +++ b/src/AliasVault.WebApp/Services/AliasService.cs @@ -22,8 +22,8 @@ public class AliasService(HttpClient httpClient) /// Identity object. public async Task GenerateRandomIdentityAsync() { - var identity = await httpClient.GetFromJsonAsync("api/Identity/generate"); - if (identity == null) + var identity = await httpClient.GetFromJsonAsync("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("api/Alias", aliasObject); + var returnObject = await httpClient.PutAsJsonAsync("api/v1/Alias", aliasObject); return await returnObject.Content.ReadFromJsonAsync(); } catch @@ -59,7 +59,7 @@ public class AliasService(HttpClient httpClient) { try { - var returnObject = await httpClient.PostAsJsonAsync("api/Alias/" + id, aliasObject); + var returnObject = await httpClient.PostAsJsonAsync("api/v1/Alias/" + id, aliasObject); return await returnObject.Content.ReadFromJsonAsync(); } catch @@ -77,7 +77,7 @@ public class AliasService(HttpClient httpClient) { try { - return await httpClient.GetFromJsonAsync("api/Alias/" + aliasId); + return await httpClient.GetFromJsonAsync("api/v1/Alias/" + aliasId); } catch { @@ -93,7 +93,7 @@ public class AliasService(HttpClient httpClient) { try { - return await httpClient.GetFromJsonAsync>("api/Alias/items"); + return await httpClient.GetFromJsonAsync>("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 {