diff --git a/Linguard/Auth/Exceptions/LoginException.cs b/Linguard/Auth/Exceptions/LoginException.cs new file mode 100644 index 0000000..854b36f --- /dev/null +++ b/Linguard/Auth/Exceptions/LoginException.cs @@ -0,0 +1,5 @@ +namespace Auth.Exceptions; + +public class LoginException : Exception { + public LoginException(string message) : base(message) { } +} \ No newline at end of file diff --git a/Linguard/Auth/Models/Credentials.cs b/Linguard/Auth/Models/Credentials.cs index 9b47b98..f675e4c 100644 --- a/Linguard/Auth/Models/Credentials.cs +++ b/Linguard/Auth/Models/Credentials.cs @@ -1,10 +1,6 @@ namespace Auth.Models; public class Credentials : ICredentials { - public Credentials(string login, string password) { - Login = login; - Password = password; - } - public string Login { get; } - public string Password { get; } + public string Login { get; set; } + public string Password { get; set; } } \ No newline at end of file diff --git a/Linguard/Auth/Models/ICredentials.cs b/Linguard/Auth/Models/ICredentials.cs index 59b5166..315db02 100644 --- a/Linguard/Auth/Models/ICredentials.cs +++ b/Linguard/Auth/Models/ICredentials.cs @@ -1,6 +1,6 @@ namespace Auth.Models; public interface ICredentials { - public string Login { get; } - public string Password { get;} + public string Login { get; set; } + public string Password { get; set; } } \ No newline at end of file diff --git a/Linguard/Auth/Models/IToken.cs b/Linguard/Auth/Models/IToken.cs deleted file mode 100644 index 258d59e..0000000 --- a/Linguard/Auth/Models/IToken.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Auth.Models; - -public interface IToken { - public string Value { get; } - public DateTime ValidUntil { get; } -} \ No newline at end of file diff --git a/Linguard/Auth/Models/Token.cs b/Linguard/Auth/Models/Token.cs deleted file mode 100644 index d870a08..0000000 --- a/Linguard/Auth/Models/Token.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Auth.Models; - -public class Token : IToken { - public Token(string value, DateTime validUntil) { - Value = value; - ValidUntil = validUntil; - } - - public string Value { get; } - public DateTime ValidUntil { get; } -} \ No newline at end of file diff --git a/Linguard/Auth/Services/IAuthService.cs b/Linguard/Auth/Services/IAuthService.cs deleted file mode 100644 index d1d3652..0000000 --- a/Linguard/Auth/Services/IAuthService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Auth.Models; - -namespace Auth.Services; - -public interface IAuthService { - IToken Login(ICredentials credentials); - void Logout(); -} \ No newline at end of file diff --git a/Linguard/Core/Managers/ConfigurationManagerBase.cs b/Linguard/Core/Managers/ConfigurationManagerBase.cs index 891bd84..9128708 100644 --- a/Linguard/Core/Managers/ConfigurationManagerBase.cs +++ b/Linguard/Core/Managers/ConfigurationManagerBase.cs @@ -23,7 +23,6 @@ protected ConfigurationManagerBase(IConfiguration configuration, IWorkingDirecto public IConfiguration Configuration { get; set; } public IWorkingDirectory WorkingDirectory { get; set; } - public bool IsSetupNeeded { get; set; } = true; public void LoadDefaults() { LoadWebDefaults(); @@ -68,7 +67,6 @@ private void LoadWireguardDefaults() { public abstract void Load(); public void Save() { - IsSetupNeeded = false; ApplyChanges(); DoSave(); } diff --git a/Linguard/Core/Managers/IConfigurationManager.cs b/Linguard/Core/Managers/IConfigurationManager.cs index df1301a..4a446b9 100644 --- a/Linguard/Core/Managers/IConfigurationManager.cs +++ b/Linguard/Core/Managers/IConfigurationManager.cs @@ -17,10 +17,6 @@ public interface IConfigurationManager { /// IWorkingDirectory WorkingDirectory { get; set; } /// - /// Flag used to tell whether the initial setup has been completed. - /// - bool IsSetupNeeded { get; set; } - /// /// Load default options. /// void LoadDefaults(); diff --git a/Linguard/Log/SimpleFileLogger.cs b/Linguard/Log/SimpleFileLogger.cs index 740f606..8554a4a 100644 --- a/Linguard/Log/SimpleFileLogger.cs +++ b/Linguard/Log/SimpleFileLogger.cs @@ -11,8 +11,11 @@ public class SimpleFileLogger : ILinguardLogger { public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (!IsEnabled(logLevel)) return; - Target?.WriteLine($"{DateTime.Now.ToString(DateTimeFormat)} [{logLevel.ToString().ToUpper()}] " + - $"{formatter(state, exception)}"); + var message = $"{DateTime.Now.ToString(DateTimeFormat)} [{logLevel.ToString().ToUpper()}] " + + $"{formatter(state, exception)}"; + if (exception != default) message += $"{Environment.NewLine}The following exception was raised:" + + $"{Environment.NewLine}{exception}"; + Target?.WriteLine(message); } public bool IsEnabled(LogLevel logLevel) { @@ -25,7 +28,9 @@ public bool IsEnabled(LogLevel logLevel) { public static class Extensions { public static ILoggingBuilder AddSimpleFileLogger(this ILoggingBuilder builder) { - builder.Services.TryAddSingleton(); + var logger = new SimpleFileLogger(); + builder.Services.TryAddSingleton(logger); + builder.Services.TryAddSingleton(logger); return builder; } diff --git a/Linguard/Web/App.razor b/Linguard/Web/App.razor index 3d6352f..7c87d23 100644 --- a/Linguard/Web/App.razor +++ b/Linguard/Web/App.razor @@ -1,12 +1,15 @@ - - - - - - - Not found - -

Oops, it looks like there's nothing here.

-
-
-
\ No newline at end of file +@using Linguard.Web.Shared.Layouts + + + + + + + + Not found + +

Oops, it looks like there's nothing here.

+
+
+
+
diff --git a/Linguard/Web/Auth/ApplicationDbContext.cs b/Linguard/Web/Auth/ApplicationDbContext.cs new file mode 100644 index 0000000..cb02c07 --- /dev/null +++ b/Linguard/Web/Auth/ApplicationDbContext.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace Linguard.Web.Auth; + +public class ApplicationDbContext : IdentityDbContext { + public ApplicationDbContext(DbContextOptions options) + : base(options) { + } +} \ No newline at end of file diff --git a/Linguard/Web/Auth/AuthenticationCookieFormatBase.cs b/Linguard/Web/Auth/AuthenticationCookieFormatBase.cs new file mode 100644 index 0000000..69397d6 --- /dev/null +++ b/Linguard/Web/Auth/AuthenticationCookieFormatBase.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Authentication.Cookies; + +namespace Linguard.Web.Auth; + +public class AuthenticationCookieFormatBase : IAuthenticationCookieFormat { + public AuthenticationCookieFormatBase(string scheme, string name) { + Scheme = scheme; + Name = name; + } + public string Scheme { get; } + public string Name { get; } +} + +public static class AuthenticationCookieFormat { + public static readonly IAuthenticationCookieFormat Default = + new AuthenticationCookieFormatBase(CookieAuthenticationDefaults.AuthenticationScheme, "Auth"); +} \ No newline at end of file diff --git a/Linguard/Web/Auth/IAuthenticationCookieFormat.cs b/Linguard/Web/Auth/IAuthenticationCookieFormat.cs new file mode 100644 index 0000000..12a6a85 --- /dev/null +++ b/Linguard/Web/Auth/IAuthenticationCookieFormat.cs @@ -0,0 +1,6 @@ +namespace Linguard.Web.Auth; + +public interface IAuthenticationCookieFormat { + public string Scheme { get; } + public string Name { get; } +} \ No newline at end of file diff --git a/Linguard/Web/Auth/IdentityAuthenticationStateProvider.cs b/Linguard/Web/Auth/IdentityAuthenticationStateProvider.cs new file mode 100644 index 0000000..37d0687 --- /dev/null +++ b/Linguard/Web/Auth/IdentityAuthenticationStateProvider.cs @@ -0,0 +1,55 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; + +namespace Linguard.Web.Auth; + +public class IdentityAuthenticationStateProvider + : RevalidatingServerAuthenticationStateProvider where TUser : class { + private readonly IServiceScopeFactory _scopeFactory; + private readonly IdentityOptions _options; + + public IdentityAuthenticationStateProvider( + ILoggerFactory loggerFactory, + IServiceScopeFactory scopeFactory, + IOptions optionsAccessor) + : base(loggerFactory) { + _scopeFactory = scopeFactory; + _options = optionsAccessor.Value; + } + + protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30); + + protected override async Task ValidateAuthenticationStateAsync( + AuthenticationState authenticationState, CancellationToken cancellationToken) { + // Get the user manager from a new scope to ensure it fetches fresh data + var scope = _scopeFactory.CreateScope(); + try { + var userManager = scope.ServiceProvider.GetRequiredService>(); + return await ValidateSecurityStampAsync(userManager, authenticationState.User); + } + finally { + if (scope is IAsyncDisposable asyncDisposable) { + await asyncDisposable.DisposeAsync(); + } + else { + scope.Dispose(); + } + } + } + + private async Task ValidateSecurityStampAsync(UserManager userManager, ClaimsPrincipal principal) { + var user = await userManager.GetUserAsync(principal); + if (user == null) { + return false; + } + if (!userManager.SupportsUserSecurityStamp) { + return true; + } + var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType); + var userStamp = await userManager.GetSecurityStampAsync(user); + return principalStamp == userStamp; + } +} \ No newline at end of file diff --git a/Linguard/Web/Helpers/IWebHelper.cs b/Linguard/Web/Helpers/IWebHelper.cs new file mode 100644 index 0000000..87f68cc --- /dev/null +++ b/Linguard/Web/Helpers/IWebHelper.cs @@ -0,0 +1,11 @@ +using Linguard.Core.Models.Wireguard; + +namespace Linguard.Web.Helpers; + +public interface IWebHelper { + Task Download(string data, string filename); + Task DownloadConfiguration(); + Task DownloadWireguardModel(IWireguardPeer peer); + void RemoveWireguardModel(IWireguardPeer peer); + byte[] GetQrCode(IWireguardPeer peer); +} \ No newline at end of file diff --git a/Linguard/Web/Helpers/WebHelper.cs b/Linguard/Web/Helpers/WebHelper.cs new file mode 100644 index 0000000..0d31955 --- /dev/null +++ b/Linguard/Web/Helpers/WebHelper.cs @@ -0,0 +1,71 @@ +using System.Text; +using Linguard.Core.Configuration; +using Linguard.Core.Managers; +using Linguard.Core.Models.Wireguard; +using Linguard.Core.Services; +using Linguard.Core.Utils; +using Linguard.Core.Utils.Wireguard; +using Microsoft.JSInterop; +using QRCoder; + +namespace Linguard.Web.Helpers; + +public class WebHelper : IWebHelper { + + public WebHelper(IJSRuntime jsRuntime, IWireguardService wireguardService, + IConfigurationManager configurationManager, QRCodeGenerator qrCodeGenerator) { + JsRuntime = jsRuntime; + WireguardService = wireguardService; + ConfigurationManager = configurationManager; + QrCodeGenerator = qrCodeGenerator; + } + + private IJSRuntime JsRuntime { get; } + private IWireguardService WireguardService { get; } + private IConfigurationManager ConfigurationManager { get; } + private IWireguardConfiguration Configuration => ConfigurationManager.Configuration.Wireguard; + private QRCodeGenerator QrCodeGenerator {get; } + + public async Task Download(string data, string filename) { + var bytes = Encoding.UTF8.GetBytes(data); + var fileStream = new MemoryStream(bytes); + using var streamRef = new DotNetStreamReference(fileStream); + await JsRuntime.InvokeVoidAsync("downloadFileFromStream", filename, streamRef); + } + + public Task DownloadConfiguration() { + return Download(ConfigurationManager.Export(), $"{AssemblyInfo.Product.ToLower()}.config"); + } + + public Task DownloadWireguardModel(IWireguardPeer peer) { + return Download(WireguardUtils.GenerateWireguardConfiguration(peer), $"{peer.Name}.conf"); + } + + public void RemoveWireguardModel(IWireguardPeer peer) { + switch (peer) { + case Client client: + RemoveClient(client); + break; + case Interface iface: + RemoveInterface(iface); + break; + } + ConfigurationManager.Save(); + } + + public byte[] GetQrCode(IWireguardPeer peer) { + var qrCodeData = QrCodeGenerator.CreateQrCode( + WireguardUtils.GenerateWireguardConfiguration(peer), QRCodeGenerator.ECCLevel.Q); + return new PngByteQRCode(qrCodeData).GetGraphic(20); + } + + private void RemoveClient(Client client) { + WireguardService.RemoveClient(client); + Configuration.GetInterface(client)?.Clients.Remove(client); + } + + private void RemoveInterface(Interface iface) { + Configuration.Interfaces.Remove(iface); + WireguardService.RemoveInterface(iface); + } +} \ No newline at end of file diff --git a/Linguard/Web/Pages/AddInterface.razor b/Linguard/Web/Pages/AddInterface.razor index 74dffbc..9f815d7 100644 --- a/Linguard/Web/Pages/AddInterface.razor +++ b/Linguard/Web/Pages/AddInterface.razor @@ -6,10 +6,11 @@ @using Linguard.Web.Services @using FluentValidation @using Linguard.Core.Configuration +@using Linguard.Web.Helpers @inject IConfigurationManager _configurationManager @inject IWireguardService _wireguardService -@inject IWebService _webService; +@inject IWebHelper _webHelper; @inject NotificationService _notificationService @inject NavigationManager _navigationManager @inject IJSRuntime _js diff --git a/Linguard/Web/Pages/EditInterface.razor b/Linguard/Web/Pages/EditInterface.razor index e4daf07..d31cbbb 100644 --- a/Linguard/Web/Pages/EditInterface.razor +++ b/Linguard/Web/Pages/EditInterface.razor @@ -8,10 +8,11 @@ @using Linguard.Web.Services @using FluentValidation @using Linguard.Core.Configuration +@using Linguard.Web.Helpers @inject IConfigurationManager _configurationManager @inject IWireguardService _wireguardService -@inject IWebService _webService; +@inject IWebHelper _webHelper; @inject NotificationService _notificationService @inject DialogService _dialogService @inject NavigationManager _navigationManager diff --git a/Linguard/Web/Pages/ImportInterface.razor b/Linguard/Web/Pages/ImportInterface.razor index 352557d..b6ebd9d 100644 --- a/Linguard/Web/Pages/ImportInterface.razor +++ b/Linguard/Web/Pages/ImportInterface.razor @@ -6,6 +6,7 @@ @using Linguard.Web.Services @using FluentValidation @using Linguard.Core.Configuration +@using Linguard.Web.Helpers @($"{AssemblyInfo.Product} | {Title}") @@ -47,7 +48,7 @@ @inject IConfigurationManager _configurationManager @inject IWireguardService _wireguardService -@inject IWebService _webService; +@inject IWebHelper _webHelper; @inject NotificationService _notificationService @inject NavigationManager _navigationManager @inject IJSRuntime _js diff --git a/Linguard/Web/Pages/Login.razor b/Linguard/Web/Pages/Login.razor new file mode 100644 index 0000000..b7124c6 --- /dev/null +++ b/Linguard/Web/Pages/Login.razor @@ -0,0 +1,92 @@ +@page "/login" +@layout LoginLayout +@using Linguard.Core.Utils +@using Linguard.Web.Shared.Layouts +@using Microsoft.AspNetCore.WebUtilities +@using global::Auth.Models +@using Linguard.Web.Services + +@($"{AssemblyInfo.Product} | {Title}") + + + + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ +@inject NotificationService _notificationService +@inject ILogger _logger +@inject NavigationManager _navigationManager +@inject IAuthenticationService _authenticationService + +@code { + const string Title = "Welcome!"; + + private readonly ICredentials _credentials = new Credentials(); + + private async void DoLogin(ICredentials credentials) { + try { + var result = await _authenticationService.Login(credentials); + if (result.User.Identity is {IsAuthenticated: false }) { + _logger.LogError("Login failed: unable to authenticate user."); + _notificationService.Notify(new NotificationMessage { + Severity = NotificationSeverity.Error, + Summary = "Failed to log in", + Detail = $"User {result.User.Identity.Name} could not be logged in." + }); + return; + } + Redirect(); + } + catch (Exception e) { + _logger.LogError(e, "Login failed: unable to authenticate user."); + _notificationService.Notify(new NotificationMessage { + Severity = NotificationSeverity.Error, + Summary = "Failed to log in", + Detail = e.Message + }); + } + } + + private void Redirect() { + _logger.LogInformation("Login completed. Redirecting..."); + var query = _navigationManager.ToAbsoluteUri(_navigationManager.Uri).Query; + var hasReturnUrl = QueryHelpers.ParseQuery(query).TryGetValue("returnUrl", out var returnUrl); + var newUrl = hasReturnUrl ? returnUrl.ToString() : "/"; + _navigationManager.NavigateTo(newUrl); + } +} diff --git a/Linguard/Web/Pages/Settings.razor b/Linguard/Web/Pages/Settings.razor index fcd8629..e57e7b8 100644 --- a/Linguard/Web/Pages/Settings.razor +++ b/Linguard/Web/Pages/Settings.razor @@ -3,6 +3,7 @@ @using Linguard.Core.Managers @using Linguard.Core.Configuration @using Linguard.Log +@using Linguard.Web.Helpers @using Linguard.Web.Services @($"{AssemblyInfo.Product} | {Title}") @@ -32,7 +33,7 @@ ButtonStyle="ButtonStyle.Primary"/> + Click="@_webHelper.DownloadConfiguration" /> @@ -40,8 +41,8 @@ @inject IConfigurationManager _configurationManager @inject NotificationService _notificationService -@inject IWebService _webService -@inject ILinguardLogger _logger +@inject IWebHelper _webHelper +@inject ILogger _logger @code { const string Title = "Settings"; diff --git a/Linguard/Web/Pages/Setup.razor b/Linguard/Web/Pages/Setup.razor index 5cffb2e..9737e71 100644 --- a/Linguard/Web/Pages/Setup.razor +++ b/Linguard/Web/Pages/Setup.razor @@ -4,7 +4,9 @@ @using Linguard.Core.Managers @using Linguard.Core.Configuration @using Linguard.Log +@using Linguard.Web.Helpers @using Linguard.Web.Services +@using Linguard.Web.Shared.Layouts @($"{AssemblyInfo.Product} | {Title}") @@ -35,9 +37,10 @@ @inject IConfigurationManager _configurationManager @inject NotificationService _notificationService -@inject IWebService _webService -@inject ILinguardLogger _logger +@inject IWebHelper _webHelper +@inject ILogger _logger @inject NavigationManager _navigationManager +@inject IWebService _webService @code { const string Title = "Setup"; @@ -52,6 +55,7 @@ _logger.LogDebug("Saving configuration..."); _configurationManager.Configuration = (IConfiguration)configuration.Clone(); _configurationManager.Save(); + _webService.IsSetupNeeded = false; _logger.LogInformation("Setup completed. Redirecting..."); _navigationManager.NavigateTo("/"); } diff --git a/Linguard/Web/Pages/Wireguard.razor b/Linguard/Web/Pages/Wireguard.razor index 9b22876..d99b6d6 100644 --- a/Linguard/Web/Pages/Wireguard.razor +++ b/Linguard/Web/Pages/Wireguard.razor @@ -6,6 +6,7 @@ @using Linguard.Core.Utils @using Linguard.Web.Services @using Linguard.Core.Configuration +@using Linguard.Web.Helpers @($"{AssemblyInfo.Product} | {Title}") @@ -117,7 +118,7 @@ @inject IConfigurationManager _configurationManager -@inject IWebService _webService +@inject IWebHelper _webHelper @inject NotificationService _notificationService @inject DialogService _dialogService @inject NavigationManager _navigationManager diff --git a/Linguard/Web/Pages/_Host.cshtml.cs b/Linguard/Web/Pages/_Host.cshtml.cs index 659f248..3b0479c 100644 --- a/Linguard/Web/Pages/_Host.cshtml.cs +++ b/Linguard/Web/Pages/_Host.cshtml.cs @@ -14,4 +14,5 @@ public HostModel(IConfigurationManager configurationManager) { private IWebConfiguration Configuration => _configurationManager.Configuration.Web; public string Stylesheet => $"_content/Radzen.Blazor/css/{Style.Default}.css"; + } \ No newline at end of file diff --git a/Linguard/Web/Program.cs b/Linguard/Web/Program.cs index bc4a376..0383951 100644 --- a/Linguard/Web/Program.cs +++ b/Linguard/Web/Program.cs @@ -7,8 +7,14 @@ using Linguard.Core.OS; using Linguard.Core.Services; using Linguard.Log; -using Linguard.Web.Middlewares; +using Linguard.Web.Auth; +using Linguard.Web.Helpers; using Linguard.Web.Services; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using QRCoder; using Radzen; using IConfiguration = Linguard.Core.Configuration.IConfiguration; @@ -19,6 +25,8 @@ builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); +#region Core services + builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); @@ -29,22 +37,68 @@ builder.Services.AddTransient(); builder.Services.AddTransient, InterfaceValidator>(); builder.Services.AddTransient, ClientValidator>(); +#endregion + +#region Web services -builder.Services.AddTransient(); +builder.Services.AddSingleton(); +builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddScoped(); + +#region Radzen builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); + +#endregion + +#endregion + +#region Authentication + +// var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); +const string connectionString = "DataSource=app.db;Cache=Shared"; +builder.Services.AddDbContext(options => + options.UseSqlite(connectionString)); +builder.Services.AddDatabaseDeveloperPageExceptionFilter(); +builder.Services.AddIdentityCore(options => { + options.SignIn.RequireConfirmedAccount = false; + options.Password.RequireDigit = false; + options.Password.RequiredLength = 1; + options.Password.RequireUppercase = false; + options.Password.RequireNonAlphanumeric = false; + }) + .AddRoles() + .AddEntityFrameworkStores() + .AddSignInManager(); + +builder.Services.AddScoped>(); +builder.Services.AddScoped(sp => + (ServerAuthenticationStateProvider) sp.GetRequiredService()); + +var authCookieFormat = AuthenticationCookieFormat.Default; +builder.Services.AddAuthentication(options => { + options.DefaultScheme = authCookieFormat.Scheme; +}).AddCookie(authCookieFormat.Scheme, options => { + options.Cookie.Name = authCookieFormat.Name; +}); + +#endregion + +#region Logging builder.Logging.AddSimpleFileLogger(); +#endregion + var app = builder.Build(); -app.UseMiddleware(); +#region Lifetime management app.Lifetime.ApplicationStarted.Register(() => { app.Services.GetService()?.OnAppStarted(); @@ -58,9 +112,18 @@ app.Services.GetService()?.OnAppStopped(); }); +#endregion + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); + app.Services.GetService()!.LogLevel = LogLevel.Debug; + var scopeFactory = app.Services.GetService(); + var scope = scopeFactory?.CreateScope(); + var context = scope?.ServiceProvider.GetService(); + var manager = scope?.ServiceProvider.GetService>(); + context?.Database.EnsureCreated(); + manager?.CreateAsync(new IdentityUser("test"), "test"); } else { app.UseExceptionHandler("/Error"); @@ -73,6 +136,8 @@ app.UseStaticFiles(); app.UseRouting(); +app.UseAuthentication(); +app.UseAuthorization(); app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); diff --git a/Linguard/Web/Services/AuthenticationService.cs b/Linguard/Web/Services/AuthenticationService.cs new file mode 100644 index 0000000..88a5504 --- /dev/null +++ b/Linguard/Web/Services/AuthenticationService.cs @@ -0,0 +1,80 @@ +using System.Security.Claims; +using Auth.Exceptions; +using Auth.Models; +using Linguard.Web.Auth; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Microsoft.JSInterop; + +namespace Linguard.Web.Services; + +public class AuthenticationService : IAuthenticationService { + + private readonly ILogger _logger; + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly AuthenticationStateProvider _authenticationStateProvider; + private readonly IHostEnvironmentAuthenticationStateProvider _hostAuthentication; + private readonly IOptionsMonitor _cookieAuthenticationOptionsMonitor; + private readonly IJSRuntime _jsRuntime; + private readonly IAuthenticationCookieFormat _cookieFormat = AuthenticationCookieFormat.Default; + private const string JsNamespace = "authFunctions"; + + public AuthenticationService(ILogger logger, UserManager userManager, SignInManager signInManager, + AuthenticationStateProvider authenticationStateProvider, + IHostEnvironmentAuthenticationStateProvider hostAuthentication, + IOptionsMonitor cookieAuthenticationOptionsMonitor, IJSRuntime jsRuntime) { + _logger = logger; + _userManager = userManager; + _signInManager = signInManager; + _authenticationStateProvider = authenticationStateProvider; + _hostAuthentication = hostAuthentication; + _cookieAuthenticationOptionsMonitor = cookieAuthenticationOptionsMonitor; + _jsRuntime = jsRuntime; + } + + public async Task Login(ICredentials credentials) { + _logger.LogInformation($"Logging in user '{credentials.Login}'..."); + var user = await _userManager.FindByNameAsync(credentials.Login); + var valid= await _signInManager.UserManager.CheckPasswordAsync(user, credentials.Password); + if (!valid) { + throw new LoginException("Invalid credentials"); + } + var principal = await _signInManager.CreateUserPrincipalAsync(user); + var identity = new ClaimsIdentity(principal.Claims, _cookieFormat.Scheme); + principal = new ClaimsPrincipal(identity); + _signInManager.Context.User = principal; + _hostAuthentication.SetAuthenticationState(Task.FromResult(new AuthenticationState(principal))); + // now the authState is updated + var authState = await _authenticationStateProvider.GetAuthenticationStateAsync(); + _logger.LogInformation($"User '{credentials.Login}' was successfully logged in."); + await SetLoginCookie(principal); + return authState; + } + + /// + /// Create an encrypted authorization ticket and store it as a cookie. + /// + /// + /// + private ValueTask SetLoginCookie(ClaimsPrincipal principal) { + // this is where we create a ticket, encrypt it, and invoke a JS method to save the cookie + var options = _cookieAuthenticationOptionsMonitor.Get(_cookieFormat.Scheme); + var ticket = new AuthenticationTicket(principal, default, _cookieFormat.Scheme); + var value = options.TicketDataFormat.Protect(ticket); + return _jsRuntime.InvokeVoidAsync($"{JsNamespace}.setCookie", _cookieFormat.Name, + value, options.ExpireTimeSpan.TotalSeconds); + } + + public async void Logout() { + var username = _signInManager.Context.User.Identity?.Name; + _logger.LogInformation($"Logging out user '{username}'..."); + var principal = _signInManager.Context.User = new ClaimsPrincipal(new ClaimsIdentity()); + _hostAuthentication.SetAuthenticationState(Task.FromResult(new AuthenticationState(principal))); + await _jsRuntime.InvokeVoidAsync($"{JsNamespace}.deleteCookie", _cookieFormat.Name); + _logger.LogInformation($"User '{username}' was successfully logged out."); + } +} \ No newline at end of file diff --git a/Linguard/Web/Services/IAuthenticationService.cs b/Linguard/Web/Services/IAuthenticationService.cs new file mode 100644 index 0000000..f1f7aef --- /dev/null +++ b/Linguard/Web/Services/IAuthenticationService.cs @@ -0,0 +1,18 @@ +using Auth.Models; +using Microsoft.AspNetCore.Components.Authorization; + +namespace Linguard.Web.Services; + +public interface IAuthenticationService { + /// + /// Try to login a user given its credentials. + /// + /// + /// The authentication result. + Task Login(ICredentials credentials); + /// + /// Logout the currently signed in user. + /// + /// + void Logout(); +} \ No newline at end of file diff --git a/Linguard/Web/Services/IWebService.cs b/Linguard/Web/Services/IWebService.cs index 9aa763d..785e258 100644 --- a/Linguard/Web/Services/IWebService.cs +++ b/Linguard/Web/Services/IWebService.cs @@ -1,11 +1,8 @@ -using Linguard.Core.Models.Wireguard; - -namespace Linguard.Web.Services; +namespace Linguard.Web.Services; public interface IWebService { - Task Download(string data, string filename); - Task DownloadConfiguration(); - Task DownloadWireguardModel(IWireguardPeer peer); - void RemoveWireguardModel(IWireguardPeer peer); - byte[] GetQrCode(IWireguardPeer peer); + /// + /// Flag used to tell whether the initial setup has been completed. + /// + bool IsSetupNeeded { get; set; } } \ No newline at end of file diff --git a/Linguard/Web/Services/LifetimeService.cs b/Linguard/Web/Services/LifetimeService.cs index 65de1ff..12c3963 100644 --- a/Linguard/Web/Services/LifetimeService.cs +++ b/Linguard/Web/Services/LifetimeService.cs @@ -7,6 +7,8 @@ using Linguard.Core.Services; using Linguard.Core.Utils; using Linguard.Log; +using Linguard.Web.Auth; +using Microsoft.EntityFrameworkCore; namespace Linguard.Web.Services; @@ -20,31 +22,42 @@ public class LifetimeService : ILifetimeService { private readonly ISystemWrapper _system; private readonly IWireguardService _wireguardService; private readonly IConfigurationManager _configurationManager; + private readonly IWebService _webService; + private readonly IServiceScope _scope; private IWireguardConfiguration Configuration => _configurationManager.Configuration.Wireguard; #endregion public LifetimeService(IConfigurationManager configurationManager, IWireguardService wireguardService, - ILinguardLogger logger, ISystemWrapper system) { + ILinguardLogger logger, ISystemWrapper system, IWebService webService, IServiceScopeFactory scopeFactory) { _configurationManager = configurationManager; _wireguardService = wireguardService; _logger = logger; _system = system; + _webService = webService; + _scope = scopeFactory.CreateScope(); } public void OnAppStarted() { - _logger.LogInformation($"Booting up {AssemblyInfo.Product}..."); + _logger.LogInformation("Booting up..."); + InitializeDatabases(); LoadConfiguration(); StartInterfaces(); } + private void InitializeDatabases() { + _logger.LogInformation("Initializing databases..."); + var context = _scope.ServiceProvider.GetService(); + context?.Database.EnsureCreated(); + } + public void OnAppStopping() { - _logger.LogInformation($"Shutting down {AssemblyInfo.Product}..."); + _logger.LogInformation("Shutting down..."); StopInterfaces(); } public void OnAppStopped() { - _logger.LogInformation($"{AssemblyInfo.Product}'s shutdown completed."); + _logger.LogInformation("Shutdown completed."); } #region Auxiliary methods @@ -53,7 +66,7 @@ private void LoadConfiguration() { _configurationManager.WorkingDirectory.BaseDirectory = GetWorkingDirectory(); try { _configurationManager.Load(); - _configurationManager.IsSetupNeeded = false; + _webService.IsSetupNeeded = false; _logger.LogInformation("Configuration loaded."); } catch (ConfigurationNotLoadedError e) { diff --git a/Linguard/Web/Services/WebService.cs b/Linguard/Web/Services/WebService.cs index fa6b7b2..e5d9d5e 100644 --- a/Linguard/Web/Services/WebService.cs +++ b/Linguard/Web/Services/WebService.cs @@ -1,71 +1,5 @@ -using System.Text; -using Linguard.Core.Configuration; -using Linguard.Core.Managers; -using Linguard.Core.Models.Wireguard; -using Linguard.Core.Services; -using Linguard.Core.Utils; -using Linguard.Core.Utils.Wireguard; -using Microsoft.JSInterop; -using QRCoder; - -namespace Linguard.Web.Services; +namespace Linguard.Web.Services; public class WebService : IWebService { - - public WebService(IJSRuntime jsRuntime, IWireguardService wireguardService, - IConfigurationManager configurationManager, QRCodeGenerator qrCodeGenerator) { - JsRuntime = jsRuntime; - WireguardService = wireguardService; - ConfigurationManager = configurationManager; - QrCodeGenerator = qrCodeGenerator; - } - - private IJSRuntime JsRuntime { get; } - private IWireguardService WireguardService { get; } - private IConfigurationManager ConfigurationManager { get; } - private IWireguardConfiguration Configuration => ConfigurationManager.Configuration.Wireguard; - private QRCodeGenerator QrCodeGenerator {get; } - - public async Task Download(string data, string filename) { - var bytes = Encoding.UTF8.GetBytes(data); - var fileStream = new MemoryStream(bytes); - using var streamRef = new DotNetStreamReference(fileStream); - await JsRuntime.InvokeVoidAsync("downloadFileFromStream", filename, streamRef); - } - - public Task DownloadConfiguration() { - return Download(ConfigurationManager.Export(), $"{AssemblyInfo.Product.ToLower()}.config"); - } - - public Task DownloadWireguardModel(IWireguardPeer peer) { - return Download(WireguardUtils.GenerateWireguardConfiguration(peer), $"{peer.Name}.conf"); - } - - public void RemoveWireguardModel(IWireguardPeer peer) { - switch (peer) { - case Client client: - RemoveClient(client); - break; - case Interface iface: - RemoveInterface(iface); - break; - } - ConfigurationManager.Save(); - } - - public byte[] GetQrCode(IWireguardPeer peer) { - var qrCodeData = QrCodeGenerator.CreateQrCode( - WireguardUtils.GenerateWireguardConfiguration(peer), QRCodeGenerator.ECCLevel.Q); - return new PngByteQRCode(qrCodeData).GetGraphic(20); - } - - private void RemoveClient(Client client) { - WireguardService.RemoveClient(client); - Configuration.GetInterface(client)?.Clients.Remove(client); - } - - private void RemoveInterface(Interface iface) { - Configuration.Interfaces.Remove(iface); - WireguardService.RemoveInterface(iface); - } + public bool IsSetupNeeded { get; set; } = true; } \ No newline at end of file diff --git a/Linguard/Web/Shared/Layouts/LayoutBase.razor b/Linguard/Web/Shared/Layouts/LayoutBase.razor new file mode 100644 index 0000000..f48e7e5 --- /dev/null +++ b/Linguard/Web/Shared/Layouts/LayoutBase.razor @@ -0,0 +1,10 @@ +@inherits LayoutComponentBase + + + + @Body + + + + + \ No newline at end of file diff --git a/Linguard/Web/Shared/Layouts/LoginLayout.razor b/Linguard/Web/Shared/Layouts/LoginLayout.razor new file mode 100644 index 0000000..fd59256 --- /dev/null +++ b/Linguard/Web/Shared/Layouts/LoginLayout.razor @@ -0,0 +1,43 @@ +@using Linguard.Core.Managers +@using Linguard.Web.Services +@inherits LayoutComponentBase + + + + + + + + + + + @Body + + + + + + + + + + +
+
+
+
+
+ +@inject IConfigurationManager _configurationManager +@inject NavigationManager _navigationManager +@inject IWebService _webService + +@code { + private RadzenBody _body; + private ErrorBoundary? errorBoundary; + + protected override void OnParametersSet() { + errorBoundary?.Recover(); + } + +} diff --git a/Linguard/Web/Shared/MainLayout.razor b/Linguard/Web/Shared/Layouts/MainLayout.razor similarity index 89% rename from Linguard/Web/Shared/MainLayout.razor rename to Linguard/Web/Shared/Layouts/MainLayout.razor index 1f84164..e8fddf6 100644 --- a/Linguard/Web/Shared/MainLayout.razor +++ b/Linguard/Web/Shared/Layouts/MainLayout.razor @@ -1,7 +1,8 @@ @using Linguard.Core.Utils @using Linguard.Core.Managers -@using Linguard.Log +@using Linguard.Web.Services @inherits LayoutComponentBase +@layout LayoutBase @@ -58,7 +59,8 @@ @inject IConfigurationManager _configurationManager @inject NavigationManager _navigationManager -@inject ILinguardLogger _logger; +@inject ILogger _logger +@inject IWebService _webService @code { private RadzenSidebar _sidebar; @@ -76,10 +78,9 @@ } protected override void OnInitialized() { - if (_configurationManager.IsSetupNeeded) { - _logger.LogInformation("Setup required. Redirecting..."); - _navigationManager.NavigateTo("/setup"); - } + if (!_webService.IsSetupNeeded) return; + _logger.LogInformation("Setup required. Redirecting..."); + _navigationManager.NavigateTo("/setup"); } } diff --git a/Linguard/Web/Shared/SetupLayout.razor b/Linguard/Web/Shared/Layouts/SetupLayout.razor similarity index 83% rename from Linguard/Web/Shared/SetupLayout.razor rename to Linguard/Web/Shared/Layouts/SetupLayout.razor index d256f89..b5d1f11 100644 --- a/Linguard/Web/Shared/SetupLayout.razor +++ b/Linguard/Web/Shared/Layouts/SetupLayout.razor @@ -1,10 +1,8 @@ -@using Linguard.Core.Managers +@using Linguard.Web.Services +@layout LayoutBase @inherits LayoutComponentBase - - - @@ -30,7 +28,7 @@ -@inject IConfigurationManager _configurationManager +@inject IWebService _webService @inject NavigationManager _navigationManager @code { @@ -43,7 +41,7 @@ protected override void OnInitialized() { base.OnInitialized(); - if (!_configurationManager.IsSetupNeeded) { + if (!_webService.IsSetupNeeded) { _navigationManager.NavigateTo("/"); } } diff --git a/Linguard/Web/Shared/PeerActions.razor b/Linguard/Web/Shared/PeerActions.razor index ec58e62..11079d3 100644 --- a/Linguard/Web/Shared/PeerActions.razor +++ b/Linguard/Web/Shared/PeerActions.razor @@ -2,6 +2,7 @@ @using Linguard.Core.Services @using Linguard.Core.OS @using Linguard.Core.Models.Wireguard +@using Linguard.Web.Helpers
@@ -22,7 +23,7 @@ } + Click="() => _webHelper.DownloadWireguardModel(Peer)"/> @@ -33,7 +34,7 @@
-@inject IWebService _webService; +@inject IWebHelper _webHelper; @inject NotificationService _notificationService @inject DialogService _dialogService @inject NavigationManager _navigationManager @@ -89,7 +90,7 @@ #endregion async Task ShowQrCode(IWireguardPeer peer) { - var qr = $"data:image/png;base64, {Convert.ToBase64String(_webService.GetQrCode(peer))}" ; + var qr = $"data:image/png;base64, {Convert.ToBase64String(_webHelper.GetQrCode(peer))}" ; await _dialogService.OpenAsync($"Configuration of {peer.Name}", ds => @
@@ -119,7 +120,7 @@ void RemovePeer() { try { - _webService.RemoveWireguardModel(peer); + _webHelper.RemoveWireguardModel(peer); AfterDelete?.Invoke(); } catch (Exception e) { diff --git a/Linguard/Web/Shared/ProfileMenu.razor b/Linguard/Web/Shared/ProfileMenu.razor index 91ae931..01684da 100644 --- a/Linguard/Web/Shared/ProfileMenu.razor +++ b/Linguard/Web/Shared/ProfileMenu.razor @@ -1,11 +1,16 @@ @using Linguard.Core.Models +@using System.Security.Claims +@using Linguard.Web.Services +@using Microsoft.AspNetCore.Authentication.Cookies +@using Microsoft.AspNetCore.Identity +@using Microsoft.Extensions.Options
- + @@ -13,23 +18,42 @@
- +
+@inject ILogger _logger +@inject AuthenticationStateProvider _authenticationStateProvider @inject IJSRuntime _jsRuntime +@inject IAuthenticationService _authenticationService @code { - string user = "admin"; - string? email; - string GreetingMessage => $"Hi, {user}"; - string GravatarEmail => email ?? "user@example.com"; + string? _user; + string? _email; + string GreetingMessage => $"Hi, {_user}"; + string GravatarEmail => _email ?? "user@example.com"; async Task OnStyleChanged(string styleName) { var style = Enum.Parse