diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/App.razor b/src/Elektrifikatsiya/Elektrifikatsiya/App.razor index 9c0ed48..8286e30 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/App.razor +++ b/src/Elektrifikatsiya/Elektrifikatsiya/App.razor @@ -1,4 +1,5 @@ - +@using Blazorise.Components + @@ -16,11 +17,21 @@ { BarOptions = new() { - HorizontalHeight = "72px" + HorizontalHeight = "72px", + DarkColors = new() + { + ItemColorOptions = new() + { + ActiveBackgroundColor = "#1a237e", + ActiveColor = "#1a237e", + HoverColor = "#1a237e", + } + + } }, ColorOptions = new() { - Primary = "#0288D1", + Primary = "#1a237e", Secondary = "#A65529", Success = "#23C02E", Info = "#9BD8FE", @@ -31,7 +42,7 @@ }, BackgroundOptions = new() { - Primary = "#0288D1", + Primary = "#1a237e", Secondary = "#A65529", Success = "#23C02E", Info = "#9BD8FE", @@ -42,7 +53,12 @@ }, InputOptions = new() { - CheckColor = "#0288D1", + CheckColor = "#1a237e", + }, + TextColorOptions = new() + { + Dark = "#000000", + Primary = "#1a237e" } }; } \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Components/Layout/SideMenu.razor b/src/Elektrifikatsiya/Elektrifikatsiya/Components/Layout/SideMenu.razor index 7450e71..3691e03 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Components/Layout/SideMenu.razor +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Components/Layout/SideMenu.razor @@ -1,6 +1,6 @@  - + @@ -12,36 +12,30 @@ - - + + Dashboard + + + + + + Admin - - - - Pages - - - Simple Form - - + + + Add Plug + - - - - Apps - - - - Todo - - - + + + Settings + @@ -49,5 +43,6 @@ @code { private bool pagesBarVisible = true; - RenderFragment customIcon =@; + //TODO: replace logo png with transparent background one + RenderFragment customIcon =@; } \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Components/Layout/TopMenu.razor b/src/Elektrifikatsiya/Elektrifikatsiya/Components/Layout/TopMenu.razor deleted file mode 100644 index 2fd68fd..0000000 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Components/Layout/TopMenu.razor +++ /dev/null @@ -1,163 +0,0 @@ -@using Blazorise.Localization - - - - - - - Elektrifikatsiya - - - - - - - - Home - - - Documentation - - - - More - - - Quick-Start Guide - - - - Usage - - - - - - - Layout - - - @if ( LayoutType == "fixed-header" ) - { - - } - Fixed Header - - - @if ( LayoutType == "fixed-header-footer-only" ) - { - - } - Fixed Header and Footer only - - - @if ( LayoutType == "sider-with-header-on-top" ) - { - - } - Sider with Header on top - - - - - - - - - - - - - @foreach ( var cultureInfo in LocalizationService.AvailableCultures ) - { - - @if ( cultureInfo.IsNeutralCulture ) - { - @cultureInfo.EnglishName - } - else - { - @cultureInfo.Parent.EnglishName - } - - } - - - - - - Theme - - - - - Theme enabled - - - - - - - Gradient colors - - - Rounded elements - - - - - - - - - - - - - - - GitHub - - - - -@code { - protected override async Task OnInitializedAsync() - { - await SelectCulture( "en-US" ); - - await base.OnInitializedAsync(); - } - - Task SelectCulture( string name ) - { - LocalizationService.ChangeLanguage( name ); - - return Task.CompletedTask; - } - - private bool topbarVisible = false; - - Task OnLayoutTypeChecked( string layoutType ) - { - LayoutType = layoutType; - - return LayoutTypeChanged.InvokeAsync( layoutType ); - } - - [Parameter] public EventCallback ThemeEnabledChanged { get; set; } - - [Parameter] public EventCallback ThemeGradientChanged { get; set; } - - [Parameter] public EventCallback ThemeRoundedChanged { get; set; } - - [Parameter] public EventCallback ThemeColorChanged { get; set; } - - [Parameter] public string LayoutType { get; set; } - - [Parameter] public EventCallback LayoutTypeChanged { get; set; } - - [Inject] protected ITextLocalizerService LocalizationService { get; set; } - - [CascadingParameter] protected Theme Theme { get; set; } -} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Database/DeviceManagementDatabaseContext.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Database/DeviceManagementDatabaseContext.cs index 72891f0..85d2699 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Database/DeviceManagementDatabaseContext.cs +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Database/DeviceManagementDatabaseContext.cs @@ -1,4 +1,4 @@ -using Elektrifikatsiya.Model; +using Elektrifikatsiya.Models; using Microsoft.EntityFrameworkCore; namespace Elektrifikatsiya.Database diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Database/UserDatabaseContext.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Database/UserDatabaseContext.cs new file mode 100644 index 0000000..b3a6730 --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Database/UserDatabaseContext.cs @@ -0,0 +1,14 @@ +using Elektrifikatsiya.Models; + +using Microsoft.EntityFrameworkCore; + +namespace Elektrifikatsiya.Database; + +public class UserDatabaseContext : DbContext +{ + public UserDatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) + { + } + + public DbSet Users { get; set; } +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Elektrifikatsiya.csproj b/src/Elektrifikatsiya/Elektrifikatsiya/Elektrifikatsiya.csproj index b1f24ea..9d16e3c 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Elektrifikatsiya.csproj +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Elektrifikatsiya.csproj @@ -18,16 +18,27 @@ - - - + + + + + + + + + + + + + + - + diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Layouts/MainLayout.razor b/src/Elektrifikatsiya/Elektrifikatsiya/Layouts/MainLayout.razor index 7d66732..362810d 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Layouts/MainLayout.razor +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Layouts/MainLayout.razor @@ -2,67 +2,16 @@ @inherits LayoutComponentBase -@if ( layoutType == "fixed-header" ) -{ - - - - - - - - - - - - @Body - - - -} -else if ( layoutType == "fixed-header-footer-only" ) -{ - - - - - - @Body - - - - - -} -else if ( layoutType == "sider-with-header-on-top" ) -{ - - - - - - - - - - - - - @Body - - - - -} \ No newline at end of file + + + + + + + + + + @Body + + + \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Model/Device.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Model/Device.cs deleted file mode 100644 index 65cdf34..0000000 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Model/Device.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Net; - -namespace Elektrifikatsiya.Model; - -public class Device -{ - [Key] - public string Key { get; private set; } - [Required] - public string Name { get; private set; } = string.Empty; - [Required] - public IPAddress Address { get; private set; } - - public Device(string key, IPAddress address) - { - Key = key; - Address = address; - } -} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Models/Device.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Models/Device.cs new file mode 100644 index 0000000..41f3f38 --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Models/Device.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.Net; + +namespace Elektrifikatsiya.Models; + +public class Device +{ + [Key] + public string Key { get; private set; } + + [Required] + public string Name { get; private set; } + + [Required] + public IPAddress Address { get; private set; } + + public int PowerUsage { get; set; } + public string User { get; set; } + public string Room { get; set; } + + public Device(string key, string name, IPAddress address, int powerUsage, string user, string room) + { + Key = key; + Name = name; + Address = address; + PowerUsage = powerUsage; + User = user; + Room = room; + } +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Models/Event.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Models/Event.cs new file mode 100644 index 0000000..5c76207 --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Models/Event.cs @@ -0,0 +1,18 @@ +namespace Elektrifikatsiya.Models +{ + public class Event + { + public string EventName { get; set; } + public string Description { get; set; } + public DateTime Date { get; set; } + + //if Plug Class implemented -> Property for which plug this event is from + + public Event(string eventName, string description, DateTime date) + { + EventName = eventName; + Description = description; + Date = date; + } + } +} diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Models/Role.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Models/Role.cs new file mode 100644 index 0000000..08e4fb4 --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Models/Role.cs @@ -0,0 +1,9 @@ +namespace Elektrifikatsiya.Models; + +// Higher up = Higher permission +// Or you can set the priority manually = +public enum Role +{ + Admin, + User +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Models/User.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Models/User.cs new file mode 100644 index 0000000..175c25d --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Models/User.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Elektrifikatsiya.Models; + +public class User +{ + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + public int Id { get; private set; } + + public string Name { get; private set; } + public string PasswordHash { get; private set; } + public string? SessionToken { get; set; } + public DateTime LastLoginDate { get; set; } + public Role Role { get; set; } + + public User(string name, string passwordHash, Role role) + { + Name = name; + PasswordHash = passwordHash; + Role = role; + } +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor b/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor index d918c4b..3cd8102 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor @@ -1,31 +1,122 @@ @page "/" +@using Blazorise.Components; +@using Elektrifikatsiya.Models; @inject IVersionProvider VersionProvider -Blazorise - - Blazorise is a component library built on top of Blazor and CSS frameworks like Bootstrap, Bulma, Ant Design, and Material. It can be used to build responsive, single-page web applications. - + + + + + + + + + + @context.Item.Name + + + + + User: @context.Item.User +
+ Room: @context.Item.Room +
+ + @context.Item.PowerUsage W + +
+
+ + + +
+
+
+
+
+ + + +
+ @context.Item.EventName + @context.Item.Date +
+ @context.Item.Description +
+
+
+
+ + + + + + - - - This is a Blazorise Starting Template allowing you to quickly get started building your project! - +@code { + //TODO: insert new event here if plug produces one + List events = new List() { new Event("Placeholder", "Placeholder", DateTime.Now), new Event("Placeholder", "Placeholder", DateTime.Now), new Event("Placeholder", "Placeholder", DateTime.Now) }; + //TODO: insert new Device here if user adds one + List plugs = new List() { new Device("Placeholder", "Plug", System.Net.IPAddress.Broadcast, 60, "Oldin", "4AHINF"), new Device("Placeholder", "Plug", System.Net.IPAddress.Broadcast, 60, "Oldin", "4AHINF"), new Device("Placeholder", "Plug", System.Net.IPAddress.Broadcast, 60, "Oldin", "4AHINF") }; - - The bare minimum has been installed for you: -
    -
  • Blazorise @($"v{VersionProvider.MilestoneVersion}")
  • -
  • The Blazorise Provider you have selected
  • -
  • The corresponding icon provider
  • -
- However Blazorise has many more extensions at your disposal. - You can find them here. -
+ //code for graph + LineChart lineChart; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await HandleRedraw(); + } + } + + async Task HandleRedraw() + { + await lineChart.Clear(); + + await lineChart.AddLabelsDatasetsAndUpdate(Labels, GetLineChartDataset()); + } + + //TODO: insert dataset of current and last voltage usages + LineChartDataset GetLineChartDataset() + { + return new LineChartDataset + { + Label = "# of random number in thousands", + Data = RandomizeData(), + BackgroundColor = backgroundColors, + BorderColor = borderColors, + Fill = true, + PointRadius = 3, + CubicInterpolationMode = "monotone", + }; + } + + string[] Labels = { "Red", "Blue", "Yellow", "Green", "Purple", "Orange" }; + List backgroundColors = new List { ChartColor.FromRgba(255, 99, 132, 0.2f), ChartColor.FromRgba(54, 162, 235, 0.2f), ChartColor.FromRgba(255, 206, 86, 0.2f), ChartColor.FromRgba(75, 192, 192, 0.2f), ChartColor.FromRgba(153, 102, 255, 0.2f), ChartColor.FromRgba(255, 159, 64, 0.2f) }; + List borderColors = new List { ChartColor.FromRgba(255, 99, 132, 1f), ChartColor.FromRgba(54, 162, 235, 1f), ChartColor.FromRgba(255, 206, 86, 1f), ChartColor.FromRgba(75, 192, 192, 1f), ChartColor.FromRgba(153, 102, 255, 1f), ChartColor.FromRgba(255, 159, 64, 1f) }; + + List RandomizeData() + { + var r = new Random(DateTime.Now.Millisecond); + + return new List { + r.Next( 3, 50 ) * r.NextDouble(), + r.Next( 3, 50 ) * r.NextDouble(), + r.Next( 3, 50 ) * r.NextDouble(), + r.Next( 3, 50 ) * r.NextDouble(), + r.Next( 3, 50 ) * r.NextDouble(), + r.Next( 3, 50 ) * r.NextDouble() }; + } +} - - Please visit the official Blazorise Demo for component examples. - - - Please visit the official Blazorise Documentation to learn more about the available components. - -
diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor.cs new file mode 100644 index 0000000..ed25e93 --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Pages/Dashboard.razor.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using System.Net.Http; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Forms; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.Web.Virtualization; +using Microsoft.JSInterop; +using Elektrifikatsiya; +using Blazorise; + +namespace Elektrifikatsiya.Pages +{ + public partial class Dashboard + { + string bgcolor { get; set; } = "00f"; // (starting value) + void SetColor() + { + bgcolor = "#fd7"; + StateHasChanged(); // may not be required, but I'm at work right now, so can't check + } + } +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Pages/_Layout.cshtml b/src/Elektrifikatsiya/Elektrifikatsiya/Pages/_Layout.cshtml index a689909..45c56e9 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Pages/_Layout.cshtml +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Pages/_Layout.cshtml @@ -40,5 +40,35 @@ + + + diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Program.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Program.cs index 6e8892e..766474c 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Program.cs +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Program.cs @@ -1,14 +1,20 @@ using Blazorise; +using Blazorise.Bootstrap; using Blazorise.Icons.Material; using Blazorise.Material; using Elektrifikatsiya.Database; using Microsoft.EntityFrameworkCore; +using Elektrifikatsiya.Database; + +using Microsoft.EntityFrameworkCore; + WebApplicationBuilder builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); +builder.Services.AddDbContext(options => options.UseSqlite("Data Source=./UserDatabase.sqlite")); builder.Services.AddDbContext((options)=>options.UseSqlite("Data Source=./DeviceManagement.sqlite")); AddBlazorise(builder.Services); @@ -28,12 +34,11 @@ app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); +app.MapBlazorHub(); +app.MapFallbackToPage("/_Host"); -app.UseEndpoints(endpoints => -{ - _ = endpoints.MapBlazorHub(); - _ = endpoints.MapFallbackToPage("/_Host"); -}); +IServiceScope serviceScope = app.Services.GetRequiredService().CreateScope(); +serviceScope.ServiceProvider.GetRequiredService().Database.EnsureCreated(); app.MapControllers(); @@ -49,4 +54,10 @@ void AddBlazorise(IServiceCollection services) _ = services .AddMaterialProviders() .AddMaterialIcons(); -} \ No newline at end of file +} +builder.Services + .AddBlazorise(options => + { + options.Immediate = true; + }) + .AddBootstrapProviders(); \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Properties/launchSettings.json b/src/Elektrifikatsiya/Elektrifikatsiya/Properties/launchSettings.json index 7111a52..71c02c5 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Properties/launchSettings.json +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Properties/launchSettings.json @@ -19,7 +19,7 @@ "Docker": { "commandName": "Docker", "launchBrowser": true, - "launchUrl": "{Scheme}://{IPAdress}:{ServicePort}", + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", "publishAllPorts": true, "useSSL": true, "DockerfileRunArguments": "-p 80:80 -p 443:443" diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Services/IAuthenticationService.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Services/IAuthenticationService.cs new file mode 100644 index 0000000..611a36a --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Services/IAuthenticationService.cs @@ -0,0 +1,57 @@ +using Elektrifikatsiya.Models; + +using FluentResults; + +namespace Elektrifikatsiya.Services; + +public interface IAuthenticationService +{ + /// + /// Gets the current user. + /// + /// to and the currently authenticated user. + Task> GetUserAsync(); + + /// + /// Registers a new user. + /// + /// The name address of the user. + /// The password of the user. + /// The default role of the user. + /// A to . + + Task RegisterUserAsync(string name, string password, Role role); + + /// + /// Logs a user in. + /// + /// The name of the user. + /// The password of the user. + /// A to . + Task LoginUserAsync(string name, string password); + + /// + /// Logs the user out. + /// + /// A to . + Task LogoutUserAsync(); + + /// + /// Deletes the current user. + /// + /// A to . + Task DeleteUserAsync(); + + /// + /// Checks if a user exists. + /// + /// The name address of the user. + /// A to and a that indicates if the user exists. + Task> UserExistsAsync(string name); + + /// + /// Check if the current user is authenticated. + /// + /// A to and a that indicates if the user is authenticated. + Task> IsAuthenticated(); +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Services/IAuthorizationService.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Services/IAuthorizationService.cs new file mode 100644 index 0000000..30be3d0 --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Services/IAuthorizationService.cs @@ -0,0 +1,15 @@ +using Elektrifikatsiya.Models; + +using FluentResults; + +namespace Elektrifikatsiya.Services; + +public interface IAuthorizationService +{ + /// + /// Check if the current user is authorized. + /// + /// The minimum required role. + /// A to and a that indicates if the user is authorized. + Task> IsAuthorized(Role requiredRole); +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Services/ICookieService.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Services/ICookieService.cs new file mode 100644 index 0000000..a954ea8 --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Services/ICookieService.cs @@ -0,0 +1,13 @@ +namespace Elektrifikatsiya.Services; + +public interface ICookieService +{ + /// + /// Writes a cookie. + /// + /// The name of the cookie. + /// The value of the cookie. + /// Indicates the maximum lifetime of the cookie. + /// + Task WriteCookieAsync(string name, string value, int days); +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/AuthenticationService.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/AuthenticationService.cs new file mode 100644 index 0000000..36ce68e --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/AuthenticationService.cs @@ -0,0 +1,134 @@ +using Elektrifikatsiya.Database; +using Elektrifikatsiya.Models; +using Elektrifikatsiya.Utilities; + +using FluentResults; + +using Microsoft.EntityFrameworkCore; + +using BC = BCrypt.Net.BCrypt; + +namespace Elektrifikatsiya.Services.Implementations; + +public class AuthenticationService : IAuthenticationService +{ + private readonly UserDatabaseContext userDatabaseContext; + private readonly IHttpContextAccessor httpContextAccessor; + private readonly ICookieService cookieService; + + public AuthenticationService(UserDatabaseContext userDatabaseContext, IHttpContextAccessor httpContextAccessor, ICookieService cookieService) + { + this.userDatabaseContext = userDatabaseContext; + this.httpContextAccessor = httpContextAccessor; + this.cookieService = cookieService; + } + + public async Task DeleteUserAsync() + { + Result getUserResult = await GetUserAsync(); + + if (getUserResult.IsFailed) + { + return getUserResult.ToResult(); + } + + _ = userDatabaseContext.Users.Remove(getUserResult.Value); + return (await Result.Try(() => userDatabaseContext.SaveChangesAsync())).ToResult(); + } + + public async Task> GetUserAsync() + { + string? token = httpContextAccessor.HttpContext?.Request.Cookies["token"]?.ToString(); + + if (!TokenGenerator.ValidateToken(token, "auth")) + { + return Result.Fail("Token is not valid!"); + } + + User? user = await userDatabaseContext.Users.FirstOrDefaultAsync(u => u.SessionToken == token); + + if (user is null || DateTime.UtcNow - user.LastLoginDate > TimeSpan.FromDays(7)) + { + return Result.Fail("Token is not valid or expired!"); + } + + return user; + } + + public async Task> IsAuthenticated() + { + return (await GetUserAsync()).IsSuccess; + } + + public async Task LoginUserAsync(string name, string password) + { + User? user = await userDatabaseContext.Users.FirstOrDefaultAsync(u => u.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)); + + if (user is null) + { + return Result.Fail("User does not exist!"); + } + + if (!BC.Verify(password, user.PasswordHash)) + { + return Result.Fail("Invalid credentials!"); + } + + string newToken = TokenGenerator.GenerateToken("auth", user.Id); + + user.LastLoginDate = DateTime.UtcNow; + user.SessionToken = newToken; + + _ = await userDatabaseContext.SaveChangesAsync(); + + await cookieService.WriteCookieAsync("token", newToken, 7); + + return Result.Ok(); + } + + public async Task LogoutUserAsync() + { + string? token = httpContextAccessor.HttpContext?.Request.Cookies["token"]?.ToString(); + + await cookieService.WriteCookieAsync("token", string.Empty, 0); + + if (!TokenGenerator.ValidateToken(token, "auth")) + { + return Result.Ok(); + } + + User? user = await userDatabaseContext.Users.FirstOrDefaultAsync(u => u.SessionToken == token); + + if (user is null) + { + return Result.Ok(); + } + + user.SessionToken = null; + + _ = await userDatabaseContext.SaveChangesAsync(); + + return Result.Ok(); + } + + public async Task RegisterUserAsync(string name, string password, Role role) + { + Result userExistsResult = await UserExistsAsync(name); + + if (userExistsResult.IsFailed || userExistsResult.Value) + { + return Result.Fail("User name already exists!"); + } + + User user = new User(name, BC.HashPassword(password), role); + _ = userDatabaseContext.Users.Add(user); + + return (await Result.Try(() => userDatabaseContext.SaveChangesAsync())).ToResult(); + } + + public Task> UserExistsAsync(string name) + { + return Result.Try(() => + userDatabaseContext.Users.AnyAsync(u => u.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))); + } +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/AuthorizationService.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/AuthorizationService.cs new file mode 100644 index 0000000..e62b137 --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/AuthorizationService.cs @@ -0,0 +1,29 @@ +using Elektrifikatsiya.Models; + +using FluentResults; + +namespace Elektrifikatsiya.Services.Implementations; + +public class AuthorizationService : IAuthorizationService +{ + private readonly IAuthenticationService authenticationService; + + public AuthorizationService(IAuthenticationService authenticationService) + { + this.authenticationService = authenticationService; + } + + public async Task> IsAuthorized(Role requiredRole) + { + Result userResult = await authenticationService.GetUserAsync(); + + if (userResult.IsSuccess && userResult.Value.Role <= requiredRole) + { + return Result.Ok(true); + } + else + { + return Result.Fail("User doesn't have the required role!"); + } + } +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/CookieService.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/CookieService.cs new file mode 100644 index 0000000..d654fdc --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/CookieService.cs @@ -0,0 +1,18 @@ +using Microsoft.JSInterop; + +namespace Elektrifikatsiya.Services.Implementations; + +public class CookieService : ICookieService +{ + private readonly IJSRuntime jsRuntime; + + public CookieService(IJSRuntime jsRuntime) + { + this.jsRuntime = jsRuntime; + } + + public async Task WriteCookieAsync(string name, string value, int days) + { + _ = await jsRuntime.InvokeAsync("blazorExtensions.WriteCookie", name, value, days); + } +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Utilities/SecureStringGenerator.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Utilities/SecureStringGenerator.cs new file mode 100644 index 0000000..a37cb29 --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Utilities/SecureStringGenerator.cs @@ -0,0 +1,20 @@ +using System.Security.Cryptography; + +namespace Elektrifikatsiya.Utilities; + +public static class SecureStringGenerator +{ + public static string CreateCryptographicRandomString(int count) + { + return Convert.ToBase64String(RandomNumberGenerator.GetBytes(count)); + } + + public static string CreateCryptographicRandomString(int count, int uid) + { + List bytes = RandomNumberGenerator.GetBytes(count).ToList(); + + bytes.AddRange(BitConverter.GetBytes(uid)); + + return Convert.ToBase64String(bytes.ToArray()); + } +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Utilities/TokenGenerator.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Utilities/TokenGenerator.cs new file mode 100644 index 0000000..17e6a02 --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Utilities/TokenGenerator.cs @@ -0,0 +1,32 @@ +namespace Elektrifikatsiya.Utilities; + +public static class TokenGenerator +{ + /// + /// Creates secure token and uses an UID to make it unique. + /// + /// The type of the token. + /// The UID of a user. + /// The length of the token. + /// + public static string GenerateToken(string tokenType, int uid = 0, int length = 128) + { + return tokenType + "-" + SecureStringGenerator.CreateCryptographicRandomString(length, uid); + } + + /// + /// Checks if a token is valid. + /// + /// The token to validate. + /// The type the token should have. + /// + public static bool ValidateToken(string? token, string tokenType) + { + if (token is null) + { + return false; + } + + return token.StartsWith(tokenType + "-"); + } +} \ No newline at end of file diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/_Imports.razor b/src/Elektrifikatsiya/Elektrifikatsiya/_Imports.razor index 7fc555e..971248a 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/_Imports.razor +++ b/src/Elektrifikatsiya/Elektrifikatsiya/_Imports.razor @@ -9,3 +9,4 @@ @using Elektrifikatsiya @using Blazorise +@using Blazorise.Charts diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/wwwroot/brand-logo.png b/src/Elektrifikatsiya/Elektrifikatsiya/wwwroot/brand-logo.png deleted file mode 100644 index 3746003..0000000 Binary files a/src/Elektrifikatsiya/Elektrifikatsiya/wwwroot/brand-logo.png and /dev/null differ diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/wwwroot/logo.png b/src/Elektrifikatsiya/Elektrifikatsiya/wwwroot/logo.png new file mode 100644 index 0000000..88939cb Binary files /dev/null and b/src/Elektrifikatsiya/Elektrifikatsiya/wwwroot/logo.png differ