diff --git a/Client/Biskilog Accounting.Client.csproj b/Client/Biskilog Accounting.Client.csproj index f82683b..391ab83 100644 --- a/Client/Biskilog Accounting.Client.csproj +++ b/Client/Biskilog Accounting.Client.csproj @@ -8,6 +8,12 @@ $(AssemblyName.Replace(' ', '_')) + + true + + + true + diff --git a/Client/Pages/Auth/Login.razor b/Client/Pages/Auth/Login.razor index 498c8a3..35248d3 100644 --- a/Client/Pages/Auth/Login.razor +++ b/Client/Pages/Auth/Login.razor @@ -1,5 +1,8 @@ @layout AuthLayout @page "/" +@inject HttpClient httpclient +@inject NavigationManager navigationmanager + + + +
@@ -75,20 +81,25 @@

Please login to your account

+ @if (!string.IsNullOrEmpty(errormessage)) + { +

@errormessage

+ }
+ placeholder="Phone number or email address" @bind-value="@m_userAuth.Username" />
- +
- diff --git a/Client/Pages/Auth/Login.razor.cs b/Client/Pages/Auth/Login.razor.cs new file mode 100644 index 0000000..74b10a1 --- /dev/null +++ b/Client/Pages/Auth/Login.razor.cs @@ -0,0 +1,32 @@ +using System.Net.Http.Json; +using Biskilog_Accounting.Shared.ClientContractModels; + +namespace Biskilog_Accounting.Client.Pages.Auth +{ + public partial class Login + { + private Userauth m_userAuth; + private string errormessage; + + protected override void OnInitialized() + { + m_userAuth = new(); + base.OnInitialized(); + } + public async Task AuthUser() + { + var response = await httpclient.PostAsJsonAsync("api/authentication", m_userAuth); + if (response.IsSuccessStatusCode) + { + navigationmanager.NavigateTo("/welcome"); + } + else + { + errormessage = "Invalid Username or password"; + } + + } + + + } +} diff --git a/Client/Properties/launchSettings.json b/Client/Properties/launchSettings.json index 62cae75..94bcda6 100644 --- a/Client/Properties/launchSettings.json +++ b/Client/Properties/launchSettings.json @@ -8,23 +8,23 @@ "profiles": { "http": { "commandName": "Project", - "dotnetRunMessages": true, "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "http://localhost:5136", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "dotnetRunMessages": true }, "https": { "commandName": "Project", - "dotnetRunMessages": true, "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "https://localhost:7247;http://localhost:5136", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "dotnetRunMessages": true }, "IIS Express": { "commandName": "IISExpress", @@ -35,4 +35,4 @@ } } } -} +} \ No newline at end of file diff --git a/Server/DevBiskacdbContext.cs b/Server/BiskAcdbContext.cs similarity index 98% rename from Server/DevBiskacdbContext.cs rename to Server/BiskAcdbContext.cs index f8ce30f..10468ab 100644 --- a/Server/DevBiskacdbContext.cs +++ b/Server/BiskAcdbContext.cs @@ -5,13 +5,13 @@ using Microsoft.EntityFrameworkCore; namespace Biskilog_Accounting.Server.POSModels; -public partial class DevBiskacdbContext : DbContext +public partial class BiskAcdbContext : DbContext { - public DevBiskacdbContext() + public BiskAcdbContext() { } - public DevBiskacdbContext(DbContextOptions options) + public BiskAcdbContext(DbContextOptions options) : base(options) { } @@ -76,10 +76,6 @@ public partial class DevBiskacdbContext : DbContext public virtual DbSet Unitofmeasures { get; set; } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) -#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263. - => optionsBuilder.UseMySql("server=54.37.19.162;database=dev_biskacdb;user=biskilog;password=mefbuk-6niFsu-fytrew", Microsoft.EntityFrameworkCore.ServerVersion.Parse("10.3.38-mariadb")); - protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder diff --git a/Server/Biskilog Accounting.Server.csproj b/Server/Biskilog Accounting.Server.csproj index f10e87f..16740f5 100644 --- a/Server/Biskilog Accounting.Server.csproj +++ b/Server/Biskilog Accounting.Server.csproj @@ -8,6 +8,8 @@ $(AssemblyName.Replace(' ', '_')) + + @@ -15,6 +17,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -35,8 +38,4 @@ - - - - diff --git a/Server/DevBiskilogclientsContext.cs b/Server/BiskilogContext.cs similarity index 91% rename from Server/DevBiskilogclientsContext.cs rename to Server/BiskilogContext.cs index 87824c9..0795270 100644 --- a/Server/DevBiskilogclientsContext.cs +++ b/Server/BiskilogContext.cs @@ -4,14 +4,15 @@ using Biskilog_Accounting.Shared.ClientContractModels; using Microsoft.EntityFrameworkCore; namespace Biskilog_Accounting.Server; - -public partial class DevBiskilogclientsContext : DbContext +/// +/// This is the main EF DbContext for the Biskilog Accounting +/// +public partial class BiskilogContext : DbContext { - public DevBiskilogclientsContext() + public BiskilogContext() { } - - public DevBiskilogclientsContext(DbContextOptions options) + public BiskilogContext(DbContextOptions options) : base(options) { } @@ -30,10 +31,6 @@ public partial class DevBiskilogclientsContext : DbContext public virtual DbSet Userauths { get; set; } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) -#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263. - => optionsBuilder.UseMySql("server=54.37.19.162;database=dev_biskilogclients;user=biskilog;password=mefbuk-6niFsu-fytrew", ServerVersion.Parse("10.3.38-mariadb")); - protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder @@ -182,6 +179,15 @@ public partial class DevBiskilogclientsContext : DbContext .HasColumnName("db_name") .UseCollation("utf8mb4_general_ci") .HasCharSet("utf8mb4"); + entity.Property(e => e.Domain) + .HasMaxLength(50) + .HasDefaultValueSql("''") + .HasColumnName("domain") + .UseCollation("utf8mb4_general_ci") + .HasCharSet("utf8mb4"); + entity.Property(e => e.LastSyncDate) + .HasColumnType("datetime") + .HasColumnName("last_sync_date"); }); modelBuilder.Entity(entity => diff --git a/Server/Controllers/AuthenticationController.cs b/Server/Controllers/AuthenticationController.cs new file mode 100644 index 0000000..41b6d75 --- /dev/null +++ b/Server/Controllers/AuthenticationController.cs @@ -0,0 +1,39 @@ +using Biskilog_Accounting.Shared.ClientContractModels; +using Biskilog_Accounting.Shared.Interfaces; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.DotNet.Scaffolding.Shared.CodeModifier.CodeChange; + +namespace Biskilog_Accounting.Server.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class AuthenticationController : ControllerBase + { + private readonly IAuthService m_authService; + private readonly ITokenService m_tokenService; + public AuthenticationController(IAuthService a_authService, ITokenService a_tokenService) + { + m_authService = a_authService; + m_tokenService = a_tokenService; + } + + [HttpPost, Route("type-a")] + public async Task AuthUser(Userauth a_userData) + { + + if (a_userData != null && a_userData.Username != null && a_userData.Passsword != null) + { + var token = await m_authService.AuthenticateClient(a_userData.Username, a_userData.Passsword); + + if (token == null) + return BadRequest("Invalid credentials"); + return Ok(token.ToString()); + } + else + { + return BadRequest(); + } + } + } +} diff --git a/Server/Program.cs b/Server/Program.cs index 3af2c77..b898305 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -1,7 +1,78 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using System.Text.Json.Serialization; +using System.Text; +using Biskilog_Accounting.Server; +using Biskilog_Accounting.ServiceRepo; +using Biskilog_Accounting.Shared.Interfaces; +using Biskilog_Accounting.Server.Services; var builder = WebApplication.CreateBuilder(args); +// Add services to the container. +builder.Services.AddControllers().AddJsonOptions(x => x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles); +builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); +builder.Logging.ClearProviders(); +builder.Logging.AddConsole(); +builder.Services.AddEntityFrameworkMySql().AddDbContext(options => +{ + options.UseMySql(builder.Configuration.GetConnectionString("Connection"), new MariaDbServerVersion(new Version())); +}); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddCors(options => +{ + options.AddPolicy("CorsPolicy", + builder => builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + ); +}); +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => +{ + options.RequireHttpsMetadata = false; + options.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters() + { + ValidateIssuer = true, + ValidateAudience = true, + ValidAudience = builder.Configuration["Jwt:Audience"], + ValidIssuer = builder.Configuration["Jwt:Issuer"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) + }; +}); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo { Title = "MyBlazor", Version = "v1" }); + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Please enter a valid token", + Name = "Authorization", + Type = SecuritySchemeType.Http, + BearerFormat = "JWT", + Scheme = "Bearer" + }); + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type=ReferenceType.SecurityScheme, + Id="Bearer" + } + }, + new string[]{} + } + }); +}); builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages(); @@ -10,19 +81,31 @@ var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { + app.UseWebAssemblyDebugging(); + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "MyBlazor v1"); + c.RoutePrefix = "api/docs"; + }); } else { + app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } - app.UseHttpsRedirection(); + app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); app.UseRouting(); +app.UseCors("CorsPolicy"); + +app.UseAuthentication(); +app.UseAuthorization(); app.MapRazorPages(); app.MapControllers(); diff --git a/Server/Properties/launchSettings.json b/Server/Properties/launchSettings.json index 5e250c1..6bad409 100644 --- a/Server/Properties/launchSettings.json +++ b/Server/Properties/launchSettings.json @@ -1,44 +1,44 @@ { + "iisExpress": { + "applicationUrl": "http://localhost:24369", + "sslPort": 44366 + }, + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:54487/", + "sslPort": 44383 + } + }, "profiles": { "http": { "commandName": "Project", "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5136", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "dotnetRunMessages": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "http://localhost:5136" + "dotnetRunMessages": true }, "https": { "commandName": "Project", "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7247;http://localhost:5136", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "dotnetRunMessages": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "https://localhost:7247;http://localhost:5136" + "dotnetRunMessages": true }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" - } - }, - "iisExpress": { - "applicationUrl": "http://localhost:24369", - "sslPort": 44366 - }, - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:54487/", - "sslPort": 44383 + } } } } \ No newline at end of file diff --git a/Server/Services/AuthenticationService.cs b/Server/Services/AuthenticationService.cs new file mode 100644 index 0000000..321f325 --- /dev/null +++ b/Server/Services/AuthenticationService.cs @@ -0,0 +1,144 @@ +using Biskilog_Accounting.Shared.ClientContractModels; +using Biskilog_Accounting.Shared.Enums; +using Biskilog_Accounting.Shared.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace Biskilog_Accounting.Server.Services +{ + public class AuthenticationService : IAuthService + { + + private readonly BiskilogContext m_context; + private readonly ITokenService m_tokenService; + + public AuthenticationService( BiskilogContext a_context, ITokenService a_tokenService) + { + m_context = a_context; + m_tokenService = a_tokenService; + } + + /// + /// Returns the status of a user account + /// + /// AuthEnums + public AuthEnums AccountStatus(int? a_id, string? a_username) + { + if (m_context.Userauths.Any(i => i.UserId == a_id || i.Username == a_username)) + { + return AuthEnums.Found; + } + else + { + return AuthEnums.NotFound; + } + } + /// + /// Autenticates a user and returns a tokenized string + /// + /// + /// + /// strings + + public async Task AuthenticateClient(string a_username, string a_password) + { + var user = await GetUserAsync(a_username, a_password); + + if (user == null) + { + return null; + } + user.LastLogin = DateTime.Now; + m_context.Userauths.Update(user); + m_context.SaveChanges(); + + + Databasemap databasemap = GetClientDB(user.ClientId); + + List businessIds = GetSiteaccesspermission(user.ClientId, user.UserId).Select(t => t.BusinessId).ToList(); + Contract? contract = GetContract(user.ClientId, businessIds); + + if (contract == null) + return AuthEnums.Invalid.ToString(); + + return m_tokenService.GenerateToken(user, contract, databasemap); + } + + /// + /// Creates a new user account + /// + /// returns AuthEnums.successful if the account was created successfully + public AuthEnums CreateUser(Userauth a_user) + { + throw new NotImplementedException(); + //if (AccountStatus(null, a_user.Username) == AuthEnums.Found) + // return AuthEnums.Registered; + + //a_user.Role = m_context.UserRoles.First(i => i.RoleId== a_user.RoleId); + //a_user.Password = BCrypt.Net.BCrypt.HashPassword(a_user.Password); + //m_context.Users.Add(a_user); + + //var result = m_context.SaveChangesAsync(); + //result.Wait(); + //if(result.Result > 0) + //{ + // return AuthEnums.Successful; + //} + //return AuthEnums.Error; + } + + /// + /// Deletes a user account + /// + /// + public void DeleteUser(int a_id) + { + throw new NotImplementedException(); + } + + public List GetClientbusiness(int a_clientId) + { + return m_context.Clientbusinesses.Where(i => i.ClientId == a_clientId).ToList(); + } + + public Databasemap GetClientDB(int a_clientId) + { + return m_context.Databasemaps.First(t => t.ClientId == a_clientId); + } + + public Contract? GetContract(int a_clientId, List a_businessId) + { + return m_context.Contracts.FirstOrDefault(c => c.ClientId == a_clientId && a_businessId.Contains(c.BusinessId!.Value) && c.EndDate >= DateTime.Now); + } + + public List GetSiteaccesspermission(int a_clientId, int a_userId) + { + return m_context.Siteaccesspermissions.Where(t => t.ClientId == a_clientId && t.UserId == a_userId).ToList(); + } + + private async Task GetUserAsync(string username, string password) + { + //Todo have complete implementation after means of creating user is done + //try + //{ + // string pa = await m_context.Userauths.Where(u => u.Username == username).Select(u => u.Password).FirstAsync(); + // bool verified = BCrypt.Net.BCrypt.Verify(password, pa); + // if (verified) + // { + + + //TODO have a complete implementation + return await m_context.Userauths.FirstAsync(u => u.Username == username && u.Passsword == password); + + // } + // else + // { + // return null; + // } + //}catch(Exception ex) + //{ + // //possible is user not found + // return null; + //} + } + } +} diff --git a/Server/Services/ConnectionService.cs b/Server/Services/ConnectionService.cs new file mode 100644 index 0000000..fa89b02 --- /dev/null +++ b/Server/Services/ConnectionService.cs @@ -0,0 +1,52 @@ +using Biskilog_Accounting.Server.POSModels; +using Biskilog_Accounting.Shared.ClientContractModels; +using Biskilog_Accounting.Shared.Enums; +using Biskilog_Accounting.Shared.Interfaces; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace Biskilog_Accounting.Server.Services +{ + public class ConnectionService : IConnectionService + { + private readonly BiskilogContext m_context; + private readonly ITokenService m_tokenService; + private readonly IConfiguration m_configuration; + + public ConnectionService(BiskilogContext a_context, ITokenService a_tokenService, IConfiguration configuration) + { + m_context = a_context; + m_tokenService = a_tokenService; + m_configuration = configuration; + } + /// + /// Prepares and returns the connection string for a client using the specified database id + /// + /// Specified database id to use + /// + public string GetClientConnectionString(int a_databaseId) + { + Databasemap? dbMap = m_context.Databasemaps.Find(a_databaseId); + if (dbMap != null) + { + string rawConString = m_configuration.GetConnectionString("PrivateConnection")!.ToString(); + return String.Format(rawConString, dbMap.Domain, dbMap.DbName); + } + return ConnectionEnums.ConnectionNotEstablished.ToString(); + } + /// + /// Prepare the DB context from the specified connection string + /// + /// + /// + /// A configured BiskAcdbContext + public object PrepareDBContext(string a_connectionString) + { + + DbContextOptionsBuilder acdbContext = new DbContextOptionsBuilder(); + acdbContext.UseMySql(a_connectionString, new MariaDbServerVersion(new Version())); + + return acdbContext; + } + } +} diff --git a/Server/appsettings.json b/Server/appsettings.json index 10f68b8..140c6ab 100644 --- a/Server/appsettings.json +++ b/Server/appsettings.json @@ -5,5 +5,14 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" -} + "ConnectionStrings": { + "Connection": "server=54.37.19.162;database=dev_biskilogclients;user=biskilog;password=mefbuk-6niFsu-fytrew", + "PrivateConnection": "server={0};database={1};user=biskilog;password=mefbuk-6niFsu-fytrew" + }, + "AllowedHosts": "*", + "JWT": { + "Key": "@@BISKILOGACCOUNTING2023DEV??//##$", + "Issuer": "AUTH SERVER", + "Audience": "BISKILOG" + } +} \ No newline at end of file diff --git a/Shared/Biskilog Accounting.Shared.csproj b/Shared/Biskilog Accounting.Shared.csproj index 34ee232..b5a3a0f 100644 --- a/Shared/Biskilog Accounting.Shared.csproj +++ b/Shared/Biskilog Accounting.Shared.csproj @@ -8,14 +8,13 @@ + + + + - - - - - diff --git a/Shared/ClientContractModels/Databasemap.cs b/Shared/ClientContractModels/Databasemap.cs index b4ad7a1..efd3d14 100644 --- a/Shared/ClientContractModels/Databasemap.cs +++ b/Shared/ClientContractModels/Databasemap.cs @@ -10,4 +10,7 @@ public partial class Databasemap public string DbName { get; set; } = null!; public int ClientId { get; set; } + public string Domain { get; set; } + + public DateTime LastSyncDate { get; set; } } diff --git a/Shared/ClientContractModels/Userauth.cs b/Shared/ClientContractModels/Userauth.cs index d8b010a..44b0d6a 100644 --- a/Shared/ClientContractModels/Userauth.cs +++ b/Shared/ClientContractModels/Userauth.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; namespace Biskilog_Accounting.Shared.ClientContractModels; @@ -9,10 +10,15 @@ public partial class Userauth public int ClientId { get; set; } + [Required] + [Display(Name = "Username")] public string? Username { get; set; } public string? Email { get; set; } + [Required] + [DataType(DataType.Password)] + [Display(Name = "Password")] public string? Passsword { get; set; } public string? PhoneNumber { get; set; } diff --git a/Shared/Enums/AuthEnums.cs b/Shared/Enums/AuthEnums.cs new file mode 100644 index 0000000..b656598 --- /dev/null +++ b/Shared/Enums/AuthEnums.cs @@ -0,0 +1,17 @@ +namespace Biskilog_Accounting.Shared.Enums +{ + public enum AuthEnums + { + Registered, + AleadyLoggedin, + WrongPassword, + NotFound, + Found, + Expired, + Invalid, + Valid, + Successful, + Error + } +} + diff --git a/Shared/Enums/ConnectionEnums.cs b/Shared/Enums/ConnectionEnums.cs new file mode 100644 index 0000000..70821de --- /dev/null +++ b/Shared/Enums/ConnectionEnums.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Biskilog_Accounting.Shared.Enums +{ + public enum ConnectionEnums + { + ConnectionEstablished, + ConnectionNotEstablished, + } +} diff --git a/Shared/Interfaces/IAuthService.cs b/Shared/Interfaces/IAuthService.cs new file mode 100644 index 0000000..70a8960 --- /dev/null +++ b/Shared/Interfaces/IAuthService.cs @@ -0,0 +1,19 @@ +using Biskilog_Accounting.Shared.ClientContractModels; + +namespace Biskilog_Accounting.Shared.Interfaces +{ + public interface IAuthService + { + /// + /// Authenticates user or client + /// + /// + /// + /// A tokenized string with relevant information on the authenticated user + Task AuthenticateClient(string a_username, string a_password); + Contract? GetContract(int a_clientId, List a_businessId); + Databasemap GetClientDB(int a_clientId); + List GetSiteaccesspermission(int a_clientId, int a_userId); + List GetClientbusiness(int a_clientId); + } +} diff --git a/Shared/Interfaces/IConnectionService.cs b/Shared/Interfaces/IConnectionService.cs new file mode 100644 index 0000000..0684c3a --- /dev/null +++ b/Shared/Interfaces/IConnectionService.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Biskilog_Accounting.Shared.Interfaces +{ + public interface IConnectionService + { + /// + /// Prepares and returns the connection string for a client using the specified database id + /// + /// Specified database id to use + /// + string GetClientConnectionString(int a_databaseId); + /// + /// Prepare the DB context from the specified connection string + /// + /// + /// A configured BiskAcdbContext + object PrepareDBContext(string a_connectionString); + } +} diff --git a/Shared/Interfaces/ITokenService.cs b/Shared/Interfaces/ITokenService.cs new file mode 100644 index 0000000..730926f --- /dev/null +++ b/Shared/Interfaces/ITokenService.cs @@ -0,0 +1,15 @@ +using Biskilog_Accounting.Shared.ClientContractModels; +using Biskilog_Accounting.Shared.Enums; + +namespace Biskilog_Accounting.Shared.Interfaces +{ + public interface ITokenService + { + AuthEnums ValidateToken(string a_token); + string GenerateToken(Userauth a_user, Contract a_clientContract, Databasemap a_database); + int? GetDatabaseIdFromToken(string a_token); + int? GetUserIdFromToken(string a_token); + string? GetUserNameFromToken(string a_token); + + } +} diff --git a/Shared/POSModels/Tbluser.cs b/Shared/POSModels/Tbluser.cs index bc03592..280e97d 100644 --- a/Shared/POSModels/Tbluser.cs +++ b/Shared/POSModels/Tbluser.cs @@ -1,14 +1,17 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; namespace Biskilog_Accounting.Shared.POSModels; public partial class Tbluser { + [Required] public string Username { get; set; } = null!; public string BranchId { get; set; } = null!; + [Required] public string? Password { get; set; } public string? Firstname { get; set; } diff --git a/Shared/ServiceRepo/TokenService.cs b/Shared/ServiceRepo/TokenService.cs new file mode 100644 index 0000000..8d3639a --- /dev/null +++ b/Shared/ServiceRepo/TokenService.cs @@ -0,0 +1,124 @@ +using Biskilog_Accounting.Shared.ClientContractModels; +using Biskilog_Accounting.Shared.Enums; +using Biskilog_Accounting.Shared.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace Biskilog_Accounting.ServiceRepo +{ + public class TokenService : ITokenService + { + private IConfiguration m_configuration { get; } + public TokenService(IConfiguration a_configuration) + { + m_configuration = a_configuration; + } + + /// + /// Validates a user access token + /// + /// AuthEnums.Valid if token is a valid and unexpired token + public AuthEnums ValidateToken(string a_token) + { + try + { + string token = a_token.Substring(6).Trim(); + var handler = new JwtSecurityTokenHandler(); + JwtSecurityToken jwtToken = (JwtSecurityToken)handler.ReadToken(token); + + if (jwtToken.ValidFrom <= DateTime.Now && jwtToken.ValidTo > DateTime.Now) + return AuthEnums.Valid; + return AuthEnums.Expired; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return AuthEnums.Invalid; + } + } + /// + /// Generates an access token based on the user + /// + /// A tokenized string + public string GenerateToken(Userauth a_user, Contract a_clientContract, Databasemap a_database) + { + try + { + //create claims details based on the user information + var claims = new[] { + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString()), + new Claim("ContractStart",a_clientContract.StartDate !.Value.ToString()), + new Claim("ContractEnd",a_clientContract.EndDate!.Value.ToString()), + new Claim("UserId", a_user.UserId.ToString()), + new Claim("Username", a_user.Username.ToString()), + new Claim("DbId",a_database.DbNo.ToString()), + new Claim("ClientId", a_user.ClientId.ToString()), + }; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(m_configuration["Jwt:Key"]!)); + + var signIn = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken(m_configuration["Jwt:Issuer"], m_configuration["Jwt:Audience"], claims, expires: DateTime.UtcNow.AddDays(14), signingCredentials: signIn); + return $"{new JwtSecurityTokenHandler().WriteToken(token)}"; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return AuthEnums.Error.ToString(); + } + } + /// + ///Deserializes the token string if valid to return the specified user role id in the token string + /// + /// + /// RoleId + public int? GetDatabaseIdFromToken(string a_token) + { + if (ValidateToken(a_token) == AuthEnums.Valid) + { + string token = a_token.Substring(6).Trim(); + var handler = new JwtSecurityTokenHandler(); + JwtSecurityToken jwtToken = (JwtSecurityToken)handler.ReadToken(token); + return int.Parse(jwtToken.Claims.First(claim => claim.Type == "DbId").Value); + } + return null; + } + /// + ///Deserializes the token string if valid to return the specified user id in the token string + /// + /// + /// UserId + public int? GetUserIdFromToken(string a_token) + { + if (ValidateToken(a_token) == AuthEnums.Valid) + { + string token = a_token.Substring(6).Trim(); + var handler = new JwtSecurityTokenHandler(); + JwtSecurityToken jwtToken = (JwtSecurityToken)handler.ReadToken(token); + return int.Parse(jwtToken.Claims.First(claim => claim.Type == "UserId").Value); + } + return null; + } + /// + ///Deserializes the token string if valid to return the specified username in the token string + /// + /// + /// Username + public string? GetUserNameFromToken(string a_token) + { + if (ValidateToken(a_token) == AuthEnums.Valid) + { + string token = a_token.Substring(6).Trim(); + var handler = new JwtSecurityTokenHandler(); + JwtSecurityToken jwtToken = (JwtSecurityToken)handler.ReadToken(token); + return jwtToken.Claims.First(claim => claim.Type == "Username").Value; + } + return null; + } + } +}