diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Database/UserDatabaseContext.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Database/UserDatabaseContext.cs index 917764f..b3a6730 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Database/UserDatabaseContext.cs +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Database/UserDatabaseContext.cs @@ -6,5 +6,9 @@ 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/Program.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Program.cs index d9004c1..fb58465 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Program.cs +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Program.cs @@ -2,11 +2,16 @@ using Blazorise; using Blazorise.Icons.Material; using Blazorise.Material; +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("/host/UserDatabase.sqlite")); AddBlazorise(builder.Services); diff --git a/src/Elektrifikatsiya/Elektrifikatsiya/Services/IAuthenticationService.cs b/src/Elektrifikatsiya/Elektrifikatsiya/Services/IAuthenticationService.cs index 84b1adf..2ccc8ec 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Services/IAuthenticationService.cs +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Services/IAuthenticationService.cs @@ -18,7 +18,7 @@ public interface IAuthenticationService /// The E-mail address of the user. /// A to . - Task RegisterUserAsync(string name, string password); + Task RegisterUserAsync(string name, string password, Role role); /// /// Logs a user in. @@ -38,7 +38,7 @@ public interface IAuthenticationService /// /// The deletion token. /// A to and a that indicates if the operation was successful. - Task DeleteUserAsync(string deletionToken); + Task DeleteUserAsync(); /// /// Checks if a user exists. 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 index 1a3c2ae..36ce68e 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/AuthenticationService.cs +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/AuthenticationService.cs @@ -1,43 +1,134 @@ -using Elektrifikatsiya.Models; +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 { - public Task DeleteUserAsync(string deletionToken) + private readonly UserDatabaseContext userDatabaseContext; + private readonly IHttpContextAccessor httpContextAccessor; + private readonly ICookieService cookieService; + + public AuthenticationService(UserDatabaseContext userDatabaseContext, IHttpContextAccessor httpContextAccessor, ICookieService cookieService) { - throw new NotImplementedException(); + this.userDatabaseContext = userDatabaseContext; + this.httpContextAccessor = httpContextAccessor; + this.cookieService = cookieService; } - public Task> GetUserAsync() + public async Task DeleteUserAsync() { - throw new NotImplementedException(); + Result getUserResult = await GetUserAsync(); + + if (getUserResult.IsFailed) + { + return getUserResult.ToResult(); + } + + _ = userDatabaseContext.Users.Remove(getUserResult.Value); + return (await Result.Try(() => userDatabaseContext.SaveChangesAsync())).ToResult(); } - public Task> IsAuthenticated() + public async Task> GetUserAsync() { - throw new NotImplementedException(); + 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 Task LoginUserAsync(string name, string password) + public async Task> IsAuthenticated() { - throw new NotImplementedException(); + return (await GetUserAsync()).IsSuccess; } - public Task LogoutUserAsync() + public async Task LoginUserAsync(string name, string password) { - throw new NotImplementedException(); + 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 Task RegisterUserAsync(string name, string password) + public async Task LogoutUserAsync() { - throw new NotImplementedException(); + 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) { - throw new NotImplementedException(); + 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 index 3a2f4c4..e62b137 100644 --- a/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/AuthorizationService.cs +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Services/Implementations/AuthorizationService.cs @@ -23,7 +23,7 @@ public class AuthorizationService : IAuthorizationService } else { - return Result.Fail("Use doesn't have the required role!"); + 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..99faf60 --- /dev/null +++ b/src/Elektrifikatsiya/Elektrifikatsiya/Utilities/TokenGenerator.cs @@ -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 + "-"); + } +} \ No newline at end of file