mirror of
https://github.com/Stefan-5422/-T5-Elektrifikatsiya.git
synced 2026-09-10 15:46:03 +02:00
Merge branch 'Development' into feature-Dashboard
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
using Elektrifikatsiya.Models;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Elektrifikatsiya.Database;
|
||||||
|
|
||||||
|
public class UserDatabaseContext : DbContext
|
||||||
|
{
|
||||||
|
public UserDatabaseContext(DbContextOptions<UserDatabaseContext> dbContextOptions) : base(dbContextOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<User> Users { get; set; }
|
||||||
|
}
|
||||||
@@ -23,11 +23,15 @@
|
|||||||
<PackageReference Include="Blazorise.Components" Version="1.2.0" />
|
<PackageReference Include="Blazorise.Components" Version="1.2.0" />
|
||||||
<PackageReference Include="Blazorise.Material" Version="1.2.0" />
|
<PackageReference Include="Blazorise.Material" Version="1.2.0" />
|
||||||
<PackageReference Include="Blazorise.Icons.Material" Version="1.2.0" />
|
<PackageReference Include="Blazorise.Icons.Material" Version="1.2.0" />
|
||||||
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
|
<PackageReference Include="FluentResults" Version="3.15.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="Middleware\" />
|
||||||
<Folder Include="Model\" />
|
<Folder Include="Model\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Elektrifikatsiya.Models;
|
||||||
|
|
||||||
|
// Higher up = Higher permission
|
||||||
|
// Or you can set the priority manually <Role> = <value>
|
||||||
|
public enum Role
|
||||||
|
{
|
||||||
|
Admin,
|
||||||
|
User
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,5 +41,34 @@
|
|||||||
<script src="js/material/material.min.js"></script>
|
<script src="js/material/material.min.js"></script>
|
||||||
<script src="_framework/blazor.server.js"></script>
|
<script src="_framework/blazor.server.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.blazorExtensions = {
|
||||||
|
WriteCookie: function(name, value, days) {
|
||||||
|
|
||||||
|
var expires;
|
||||||
|
if (days) {
|
||||||
|
var date = new Date();
|
||||||
|
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||||
|
expires = "; expires=" + date.toGMTString();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expires = "";
|
||||||
|
}
|
||||||
|
document.cookie = name + "=" + value + expires + "; path=/;SameSite=Strict";
|
||||||
|
},
|
||||||
|
|
||||||
|
ScrollToId: function(id) {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (element instanceof HTMLElement) {
|
||||||
|
element.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "start",
|
||||||
|
inline: "nearest"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,11 +3,16 @@ using Blazorise.Bootstrap;
|
|||||||
using Blazorise.Icons.Material;
|
using Blazorise.Icons.Material;
|
||||||
using Blazorise.Material;
|
using Blazorise.Material;
|
||||||
|
|
||||||
|
using Elektrifikatsiya.Database;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddRazorPages();
|
builder.Services.AddRazorPages();
|
||||||
builder.Services.AddServerSideBlazor();
|
builder.Services.AddServerSideBlazor();
|
||||||
|
builder.Services.AddDbContext<UserDatabaseContext>(options => options.UseSqlite("Data Source=./UserDatabase.sqlite"));
|
||||||
|
|
||||||
AddBlazorise(builder.Services);
|
AddBlazorise(builder.Services);
|
||||||
|
|
||||||
@@ -26,12 +31,11 @@ app.UseHttpsRedirection();
|
|||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
app.MapBlazorHub();
|
||||||
|
app.MapFallbackToPage("/_Host");
|
||||||
|
|
||||||
app.UseEndpoints(endpoints =>
|
IServiceScope serviceScope = app.Services.GetRequiredService<IServiceScopeFactory>().CreateScope();
|
||||||
{
|
serviceScope.ServiceProvider.GetRequiredService<UserDatabaseContext>().Database.EnsureCreated();
|
||||||
_ = endpoints.MapBlazorHub();
|
|
||||||
_ = endpoints.MapFallbackToPage("/_Host");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using Elektrifikatsiya.Models;
|
||||||
|
|
||||||
|
using FluentResults;
|
||||||
|
|
||||||
|
namespace Elektrifikatsiya.Services;
|
||||||
|
|
||||||
|
public interface IAuthenticationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current user.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns> <see cref="Task"/> to <see langword="await"/> and the currently authenticated user.</returns>
|
||||||
|
Task<Result<User>> GetUserAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a new user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="email">The E-mail address of the user.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> to <see langword="await"/>.</returns>
|
||||||
|
|
||||||
|
Task<Result> RegisterUserAsync(string name, string password, Role role);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a user in.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The E-mail address of the user.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> to <see langword="await"/>.</returns>
|
||||||
|
Task<Result> LoginUserAsync(string name, string password);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs the user out.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="Task"/> to <see langword="await"/>.</returns>
|
||||||
|
Task<Result> LogoutUserAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes the current user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="deletionToken">The deletion token.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> to <see langword="await"/> and a <see cref="bool"/> that indicates if the operation was successful.</returns>
|
||||||
|
Task<Result> DeleteUserAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a user exists.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The E-mail address of the user.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> to <see langword="await"/> and a <see cref="bool"/> that indicates if the user exists.</returns>
|
||||||
|
Task<Result<bool>> UserExistsAsync(string name);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the current user is authenticated.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="Task"/> to <see langword="await"/> and a <see cref="bool"/> that indicates if the user is authenticated.</returns>
|
||||||
|
Task<Result<bool>> IsAuthenticated();
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using Elektrifikatsiya.Models;
|
||||||
|
|
||||||
|
using FluentResults;
|
||||||
|
|
||||||
|
namespace Elektrifikatsiya.Services;
|
||||||
|
|
||||||
|
public interface IAuthorizationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the current user is authorized.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requiredRole">The minimum required role.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> to <see langword="await"/> and a <see cref="bool"/> that indicates if the user is authorized.</returns>
|
||||||
|
Task<Result<bool>> IsAuthorized(Role requiredRole);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Elektrifikatsiya.Services;
|
||||||
|
|
||||||
|
public interface ICookieService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a cookie.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the cookie.</param>
|
||||||
|
/// <param name="value">The value of the cookie.</param>
|
||||||
|
/// <param name="days">Indicates the maximum lifetime of the cookie.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task WriteCookieAsync(string name, string value, int days);
|
||||||
|
}
|
||||||
+134
@@ -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<Result> DeleteUserAsync()
|
||||||
|
{
|
||||||
|
Result<User> getUserResult = await GetUserAsync();
|
||||||
|
|
||||||
|
if (getUserResult.IsFailed)
|
||||||
|
{
|
||||||
|
return getUserResult.ToResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = userDatabaseContext.Users.Remove(getUserResult.Value);
|
||||||
|
return (await Result.Try(() => userDatabaseContext.SaveChangesAsync())).ToResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<User>> 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<Result<bool>> IsAuthenticated()
|
||||||
|
{
|
||||||
|
return (await GetUserAsync()).IsSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> 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<Result> 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<Result> RegisterUserAsync(string name, string password, Role role)
|
||||||
|
{
|
||||||
|
Result<bool> 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<Result<bool>> UserExistsAsync(string name)
|
||||||
|
{
|
||||||
|
return Result.Try(() =>
|
||||||
|
userDatabaseContext.Users.AnyAsync(u => u.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)));
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -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<Result<bool>> IsAuthorized(Role requiredRole)
|
||||||
|
{
|
||||||
|
Result<User> 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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<string>("blazorExtensions.WriteCookie", name, value, days);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<byte> bytes = RandomNumberGenerator.GetBytes(count).ToList();
|
||||||
|
|
||||||
|
bytes.AddRange(BitConverter.GetBytes(uid));
|
||||||
|
|
||||||
|
return Convert.ToBase64String(bytes.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace Elektrifikatsiya.Utilities;
|
||||||
|
|
||||||
|
public static class TokenGenerator
|
||||||
|
{
|
||||||
|
public static string GenerateToken(string tokenType, int uid = 0, int length = 128)
|
||||||
|
{
|
||||||
|
return tokenType + "-" + SecureStringGenerator.CreateCryptographicRandomString(length, uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ValidateToken(string? token, string tokenType)
|
||||||
|
{
|
||||||
|
if (token is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.StartsWith(tokenType + "-");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user