Use Razor to present view

This commit is contained in:
Wvader 2022-10-27 18:13:02 +01:00
parent 961ed87266
commit 29eb91c3b1
70 changed files with 1533 additions and 776 deletions

View File

@ -3,6 +3,7 @@ using BlueWest.Data.Auth;
using BlueWest.Data.Auth.Context.Users; using BlueWest.Data.Auth.Context.Users;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -35,13 +36,13 @@ namespace BlueWest.WebApi.Controllers;
/// <summary> /// <summary>
/// Signup user /// Signup user
/// </summary> /// </summary>
/// <param name="registerViewModel"></param> /// <param name="registerRequest"></param>
/// <returns></returns> /// <returns></returns>
[AllowAnonymous] [AllowAnonymous]
[HttpPost("register")] [HttpPost("register")]
public async Task<ActionResult<IdentityResult>> SignupUserAsync(RegisterViewModel registerViewModel) public async Task<ActionResult<IdentityResult>> SignupUserAsync(RegisterRequest registerRequest)
{ {
return await _authManager.CreateUserAsync(registerViewModel); return await _authManager.CreateUserAsync(registerRequest);
} }
@ -55,7 +56,7 @@ namespace BlueWest.WebApi.Controllers;
[HttpPost("login")] [HttpPost("login")]
public async Task<ActionResult<IdentityResult>> GetSessionToken(LoginRequest loginViewModel) public async Task<ActionResult<IdentityResult>> GetSessionToken(LoginRequest loginViewModel)
{ {
var (success, sessionToken, _) = await _authManager.GetSessionTokenIdByLoginRequest(loginViewModel); var (success, sessionToken, _) = await _authManager.GetSessionTokenIdByLoginRequest(loginViewModel, JwtBearerDefaults.AuthenticationScheme);
if (success) if (success)
{ {

View File

@ -54,7 +54,14 @@ namespace BlueWest.WebApi
.AllowCredentials(); .AllowCredentials();
}); });
}); });
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.Cookie.Name = ".BlueWest.Session";
options.Cookie.Domain = SessionConstants.CookieDomain;
options.Cookie.HttpOnly = true;
});
services services
.AddResponseCaching() .AddResponseCaching()
.AddControllers(options => .AddControllers(options =>
@ -71,12 +78,8 @@ namespace BlueWest.WebApi
{ {
builder.AddSimpleConsole(); builder.AddSimpleConsole();
}); });
services.AddSession(options =>
{
options.Cookie.Domain = SessionConstants.CookieDomain;
options.Cookie.HttpOnly = true;
});
services services
.AddSwaggerGen(options => .AddSwaggerGen(options =>
{ {
@ -136,20 +139,15 @@ namespace BlueWest.WebApi
); );
*/ */
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddJsonFile("config.json")
.Build();
services services
.AddSingleton<EventManager>(); .AddSingleton<EventManager>();
services.AddAuthServerServices( _configuration, _environment, configuration); services.AddAuthServerServices( _configuration, _environment);
services.AddScoped<ExchangeInterface>(); services.AddScoped<ExchangeInterface>();
services.PrepareMySqlDatabasePool(_configuration, _environment, configuration); services.PrepareMySqlDatabasePool(_configuration, _environment);
@ -180,6 +178,7 @@ namespace BlueWest.WebApi
app.UseRouting(); app.UseRouting();
app.UseCors(MyAllowSpecificOrigins); app.UseCors(MyAllowSpecificOrigins);
app.UseSession();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>

View File

@ -32,23 +32,23 @@ namespace BlueWest.WebApi
{ {
private static MySqlServerVersion GetMySqlServerVersion(int major, int minor, int build) => new (new Version(major, minor, build)); private static MySqlServerVersion GetMySqlServerVersion(int major, int minor, int build) => new (new Version(major, minor, build));
private static BlueWestConnectionString GetConnectionString(this IConfigurationRoot configurationRoot) private static string GetConnectionString(this IConfiguration configurationRoot, string db)
{ {
// Docker / No-Docker // Docker / No-Docker
var startupMode = configurationRoot["mode"]; var startupMode = configurationRoot["mode"];
if (startupMode == "docker") if (startupMode == "docker")
{ {
var config = configurationRoot.Get<ConnectionStringDocker>(); var config = configurationRoot.GetSection("ConnectionStringDocker")[db];
return config; return config;
} }
else else
{ {
var config = configurationRoot.Get<ConnectionStringNoDocker>(); var config = configurationRoot.GetSection("ConnectionStringNoDocker")[db];
return config; return config;
} }
return null; return string.Empty;
} }
/// <summary> /// <summary>
@ -60,14 +60,13 @@ namespace BlueWest.WebApi
private static DbContextOptionsBuilder GetMySqlSettings( private static DbContextOptionsBuilder GetMySqlSettings(
this DbContextOptionsBuilder optionsBuilder, this DbContextOptionsBuilder optionsBuilder,
IConfiguration configuration, IConfiguration configuration,
IConfigurationRoot configurationRoot,
IWebHostEnvironment environment) IWebHostEnvironment environment)
{ {
var sqlVersion = GetMySqlServerVersion(8, 0, 11); var sqlVersion = GetMySqlServerVersion(8, 0, 11);
// Docker / No-Docker // Docker / No-Docker
string mySqlConnectionString = configurationRoot.GetConnectionString().MySql; var mySqlConnectionString = configuration.GetConnectionString("MySQL");
if (mySqlConnectionString == string.Empty) if (mySqlConnectionString == string.Empty)
{ {
@ -106,25 +105,25 @@ namespace BlueWest.WebApi
/// <param name="environment"></param> /// <param name="environment"></param>
/// <returns></returns> /// <returns></returns>
public static IServiceCollection PrepareMySqlDatabasePool(this IServiceCollection serviceCollection, public static IServiceCollection PrepareMySqlDatabasePool(this IServiceCollection serviceCollection,
IConfiguration configuration, IWebHostEnvironment environment, IConfigurationRoot configurationRoot) IConfiguration configuration, IWebHostEnvironment environment)
{ {
return serviceCollection return serviceCollection
.AddDbContextPool<UserDbContext>(options => .AddDbContextPool<UserDbContext>(options =>
options.GetMySqlSettings(configuration, configurationRoot, environment)) options.GetMySqlSettings(configuration, environment))
.AddDbContextPool<CountryDbContext>(options => .AddDbContextPool<CountryDbContext>(options =>
options.GetMySqlSettings(configuration, configurationRoot, environment)) options.GetMySqlSettings(configuration, environment))
.AddDbContextPool<FinanceDbContext>(options => .AddDbContextPool<FinanceDbContext>(options =>
options.GetMySqlSettings(configuration, configurationRoot, environment)) options.GetMySqlSettings(configuration, environment))
.AddDbContextPool<CompanyDbContext>(options => .AddDbContextPool<CompanyDbContext>(options =>
options.GetMySqlSettings(configuration, configurationRoot, environment)) options.GetMySqlSettings(configuration, environment))
.AddDbContextPool<ApplicationUserDbContext>(options => .AddDbContextPool<ApplicationUserDbContext>(options =>
options.GetMySqlSettings(configuration, configurationRoot, environment)); options.GetMySqlSettings(configuration, environment));
} }
internal static IServiceCollection AddAuthServerServices(this IServiceCollection services, IConfiguration configuration , IWebHostEnvironment environment, IConfigurationRoot configurationRoot) internal static IServiceCollection AddAuthServerServices(this IServiceCollection services, IConfiguration configuration , IWebHostEnvironment environment)
{ {
var connectionString = configurationRoot.GetConnectionString(); var connectionString = configuration.GetConnectionString("Redis");
if (connectionString == null) if (connectionString == null)
{ {
@ -132,7 +131,7 @@ namespace BlueWest.WebApi
} }
services services
.AddSingleton(new RedisConnectionProvider(connectionString.Redis)) .AddSingleton(new RedisConnectionProvider(connectionString))
.AddScoped<IJwtTokenHandler, JwtTokenHandler>() .AddScoped<IJwtTokenHandler, JwtTokenHandler>()
.AddScoped<IJwtFactory, JwtFactory>() .AddScoped<IJwtFactory, JwtFactory>()
.AddHostedService<SessionManager>() .AddHostedService<SessionManager>()

View File

@ -1,4 +0,0 @@
{
"mode": "no-docker",
"database": "mysql"
}

View File

@ -44,4 +44,12 @@
<ProjectReference Include="..\BlueWest.Domain\BlueWest.Domain.csproj" /> <ProjectReference Include="..\BlueWest.Domain\BlueWest.Domain.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="appsettings.json">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
</Project> </Project>

View File

@ -1,5 +0,0 @@
namespace BlueWest.Data.Auth;
public class Class1
{
}

View File

@ -1,12 +1,16 @@
namespace BlueWest.WebApi.Configuration namespace BlueWest.WebApi.Configuration
{ {
public class BlueWestConnectionString public class ConnectionStringDocker
{ {
public string Redis { get; set; } public string Redis { get; set; }
public string MySql { get; set; } public string MySql { get; set; }
} }
public class ConnectionStringDocker : BlueWestConnectionString { } public class ConnectionStringNoDocker
public class ConnectionStringNoDocker : BlueWestConnectionString { } {
public string Redis { get; set; }
public string MySql { get; set; }
}
} }

View File

@ -23,7 +23,7 @@ namespace BlueWest.Data.Auth.Context.Users
public string Password { get; set; } public string Password { get; set; }
[Required] [Required]
public string Uuid { get; set; } public string Uuid { get ; set; }
/// <summary> /// <summary>
/// Gets Uuid for this login request /// Gets Uuid for this login request

View File

@ -6,7 +6,7 @@ namespace BlueWest.Data.Auth.Context.Users;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public class RegisterViewModel public class RegisterRequest
{ {
/// <summary> /// <summary>
/// Email /// Email

View File

@ -5,10 +5,17 @@ namespace BlueWest.Data.Auth
public static class SessionConstants public static class SessionConstants
{ {
/// <summary>
/// Max age for the Session
/// </summary>
public static TimeSpan DefaultSessionMaxAge = TimeSpan.FromHours(24); public static TimeSpan DefaultSessionMaxAge = TimeSpan.FromHours(24);
/// <summary>
/// API User policy Key
/// </summary>
public const string ApiNamePolicy = "ApiUser"; public const string ApiNamePolicy = "ApiUser";
public const string SessionTokenHeaderName = "x-bw2-auth"; public const string SessionTokenHeaderName = "x-bw2-auth";
public const string CookieDomain = "http://localhost:5173"; public const string CookieDomain = "https://localhost:7022";
} }
} }

View File

@ -3,12 +3,16 @@ using System.Security.Claims;
using BlueWest.Cryptography; using BlueWest.Cryptography;
using BlueWest.Data.Application; using BlueWest.Data.Application;
using BlueWest.Data.Application.Users; using BlueWest.Data.Application.Users;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using static BlueWest.Data.Auth.Context.Users.AuthConsts; using static BlueWest.Data.Auth.Context.Users.AuthConsts;
namespace BlueWest.Data.Auth.Context.Users namespace BlueWest.Data.Auth.Context.Users
{ {
/// <summary>
/// Authentication Manager for the Application Users
/// </summary>
public class AuthManager : IAuthManager public class AuthManager : IAuthManager
{ {
private readonly ApplicationUserManager _userManager; private readonly ApplicationUserManager _userManager;
@ -48,7 +52,7 @@ namespace BlueWest.Data.Auth.Context.Users
return _hasher.CreateHash(uuid, BaseCryptoItem.HashAlgorithm.SHA2_512); return _hasher.CreateHash(uuid, BaseCryptoItem.HashAlgorithm.SHA2_512);
} }
private SessionToken GetNewSessionToken(LoginRequest loginRequest, ApplicationUser user, string token) private SessionToken GetNewSessionToken(LoginRequest loginRequest, ApplicationUser user)
{ {
long timeNow = DateTimeOffset.Now.ToUnixTimeMilliseconds(); long timeNow = DateTimeOffset.Now.ToUnixTimeMilliseconds();
@ -58,8 +62,7 @@ namespace BlueWest.Data.Auth.Context.Users
UserId = user.Id, UserId = user.Id,
CreatedDate = timeNow, CreatedDate = timeNow,
IsValid = true, IsValid = true,
ValidFor = SessionConstants.DefaultSessionMaxAge.Milliseconds, ValidFor = SessionConstants.DefaultSessionMaxAge.Milliseconds
AccessToken = token
}; };
return newToken; return newToken;
@ -77,7 +80,7 @@ namespace BlueWest.Data.Auth.Context.Users
return token.IsValid; return token.IsValid;
} }
public async Task<(bool, SessionTokenUnique, ClaimsIdentity)> GetSessionTokenIdByLoginRequest(LoginRequest loginRequest) public async Task<(bool, SessionTokenUnique, ClaimsIdentity)> GetSessionTokenIdByLoginRequest(LoginRequest loginRequest, string authenticationType = JwtBearerDefaults.AuthenticationScheme)
{ {
var user = await _userManager.FindByEmailAsync(loginRequest.Email); var user = await _userManager.FindByEmailAsync(loginRequest.Email);
@ -85,24 +88,55 @@ namespace BlueWest.Data.Auth.Context.Users
if (!await _userManager.CheckPasswordAsync(user, loginRequest.Password)) return NegativeToken; if (!await _userManager.CheckPasswordAsync(user, loginRequest.Password)) return NegativeToken;
var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme); var identity = new ClaimsIdentity(authenticationType);
identity.AddClaim(new Claim(ClaimTypes.Email, user.Email)); identity.AddClaim(new Claim(ClaimTypes.Email, user.Email));
identity.AddClaim(new Claim(ClaimTypes.MobilePhone, user.PhoneNumber));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
var sessionToken = await GetSessionToken(loginRequest); var sessionToken = await GetSessionToken(loginRequest);
if (sessionToken == null || !SessionTokenIsValid(sessionToken)) if (sessionToken == null || !SessionTokenIsValid(sessionToken))
{ {
var (success, bearerToken) = await GenerateBearerToken(identity, user); var (success, bearerToken) = await GenerateBearerToken(identity, user);
var newSessionToken = GetNewSessionToken(loginRequest, user, bearerToken); var newSessionToken = GetNewSessionToken(loginRequest, user);
await _sessionCache.AddSessionToken(newSessionToken); await _sessionCache.AddSessionToken(newSessionToken);
var tokenUnique = new SessionTokenUnique(newSessionToken); var tokenUnique = new SessionTokenUnique(newSessionToken);
return OkAuth(tokenUnique, identity, success); return OkAuth(tokenUnique, identity, success);
} }
var response = new SessionTokenUnique(sessionToken); var response = new SessionTokenUnique(sessionToken);
return OkAuth(response, identity); return OkAuth(response, identity);
} }
public async Task<(bool, SessionTokenUnique, ClaimsIdentity)> GetSessionTokenIdByLoginRequestViaCookie(LoginRequest loginRequest, string authenticationType = CookieAuthenticationDefaults.AuthenticationScheme)
{
var user = await _userManager.FindByEmailAsync(loginRequest.Email);
if (user == null) return NegativeToken;
if (!await _userManager.CheckPasswordAsync(user, loginRequest.Password)) return NegativeToken;
var identity = new ClaimsIdentity(authenticationType);
identity.AddClaim(new Claim(ClaimTypes.Email, user.Email));
identity.AddClaim(new Claim(ClaimTypes.MobilePhone, user.PhoneNumber));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
var sessionToken = await GetSessionToken(loginRequest);
if (sessionToken == null || !SessionTokenIsValid(sessionToken))
{
var newSessionToken = GetNewSessionToken(loginRequest, user);
await _sessionCache.AddSessionToken(newSessionToken);
var tokenUnique = new SessionTokenUnique(newSessionToken);
return OkAuth(tokenUnique, identity);
}
var response = new SessionTokenUnique(sessionToken);
return OkAuth(response, identity);
}
private async Task<(bool, string)> GenerateBearerToken(ClaimsIdentity identity, ApplicationUser user) private async Task<(bool, string)> GenerateBearerToken(ClaimsIdentity identity, ApplicationUser user)
{ {
@ -113,6 +147,12 @@ namespace BlueWest.Data.Auth.Context.Users
} }
/// <summary>
/// Verify Password
/// </summary>
/// <param name="email"></param>
/// <param name="password"></param>
/// <returns></returns>
public async Task<bool> VerifyLoginByEmailAsync(string email, string password) public async Task<bool> VerifyLoginByEmailAsync(string email, string password)
{ {
var user = await _userManager.FindByEmailAsync(email); var user = await _userManager.FindByEmailAsync(email);
@ -120,7 +160,12 @@ namespace BlueWest.Data.Auth.Context.Users
} }
public async Task<IdentityResult> CreateUserAsync(RegisterViewModel userSignupDto) /// <summary>
/// Create user
/// </summary>
/// <param name="userSignupDto"></param>
/// <returns></returns>
public async Task<IdentityResult> CreateUserAsync(RegisterRequest userSignupDto)
{ {
userSignupDto.Password = _hasher.CreateHash(userSignupDto.Password, BaseCryptoItem.HashAlgorithm.SHA3_512);; userSignupDto.Password = _hasher.CreateHash(userSignupDto.Password, BaseCryptoItem.HashAlgorithm.SHA3_512);;
var newUser = userSignupDto.ToUser(); var newUser = userSignupDto.ToUser();

View File

@ -14,16 +14,26 @@ public interface IAuthManager
/// <summary> /// <summary>
/// CreateUserAsync /// CreateUserAsync
/// </summary> /// </summary>
/// <param name="registerViewModel"></param> /// <param name="registerRequest"></param>
/// <returns></returns> /// <returns></returns>
Task<IdentityResult> CreateUserAsync(RegisterViewModel registerViewModel); Task<IdentityResult> CreateUserAsync(RegisterRequest registerRequest);
/// <summary> /// <summary>
/// Does Login /// Does Login
/// </summary> /// </summary>
/// <param name="loginRequest"></param> /// <param name="loginRequest"></param>
/// <returns></returns> /// <returns></returns>
public Task<(bool, SessionTokenUnique, ClaimsIdentity)> GetSessionTokenIdByLoginRequest(LoginRequest loginRequest); public Task<(bool, SessionTokenUnique, ClaimsIdentity)> GetSessionTokenIdByLoginRequest(LoginRequest loginRequest, string authenticationType);
/// <summary>
/// Does Login
/// </summary>
/// <param name="loginRequest"></param>
/// <returns></returns>
public Task<(bool, SessionTokenUnique, ClaimsIdentity)> GetSessionTokenIdByLoginRequestViaCookie(LoginRequest loginRequest, string authenticationType);
} }

View File

@ -1,4 +1,5 @@
using System.Security.Claims;
using BlueWest.Data.Application.Users; using BlueWest.Data.Application.Users;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
@ -6,6 +7,12 @@ namespace BlueWest.Data.Auth.Context.Users
{ {
public interface IUserManager public interface IUserManager
{ {
/// <summary>
///
/// </summary>
/// <param name="principal"></param>
/// <returns></returns>
Task<ApplicationUser> GetUserAsync(ClaimsPrincipal principal);
/// <summary> /// <summary>
/// Create user. /// Create user.
/// </summary> /// </summary>
@ -28,8 +35,8 @@ namespace BlueWest.Data.Auth.Context.Users
/// <returns></returns> /// <returns></returns>
Task<ApplicationUser> FindByEmailAsync(string email); Task<ApplicationUser> FindByEmailAsync(string email);
string GetUserId(ClaimsPrincipal principal);
} }
} }

View File

@ -0,0 +1,24 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStringDocker": {
"MySQL": "server=db;user=blueuser;password=dXjw127124dJ;database=bluedb;",
"Redis": "redis://redisinstance:6379"
},
"ConnectionStringNoDocker": {
"MySQL": "server=localhost;user=blueuser;password=dXjw127124dJ;database=bluedb;",
"Redis": "redis://localhost:6379"
},
"AuthSettings": {
"SecretKey": "iJWHDmHLpUA283sqsfhqGbMRdRj1PVkH"
},
"JwtIssuerOptions": {
"Issuer": "SomeIssuer",
"Audience": "http://localhost:5000"
}
}

View File

@ -1,18 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.3"/>
</ItemGroup>
</Project>

View File

@ -1,3 +0,0 @@
<div class="my-component">
This component is defined in the <strong>BlueWest.Razor.Library</strong> library.
</div>

View File

@ -1,6 +0,0 @@
.my-component {
border: 2px dashed red;
padding: 1em;
margin: 1em 0;
background-image: url('background.png');
}

View File

@ -1,36 +0,0 @@
using Microsoft.JSInterop;
namespace BlueWest.Razor.Library;
// This class provides an example of how JavaScript functionality can be wrapped
// in a .NET class for easy consumption. The associated JavaScript module is
// loaded on demand when first needed.
//
// This class can be registered as scoped DI service and then injected into Blazor
// components for use.
public class ExampleJsInterop : IAsyncDisposable
{
private readonly Lazy<Task<IJSObjectReference>> moduleTask;
public ExampleJsInterop(IJSRuntime jsRuntime)
{
moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>(
"import", "./_content/BlueWest.Razor.Library/exampleJsInterop.js").AsTask());
}
public async ValueTask<string> Prompt(string message)
{
var module = await moduleTask.Value;
return await module.InvokeAsync<string>("showPrompt", message);
}
public async ValueTask DisposeAsync()
{
if (moduleTask.IsValueCreated)
{
var module = await moduleTask.Value;
await module.DisposeAsync();
}
}
}

View File

@ -1 +0,0 @@
@using Microsoft.AspNetCore.Components.Web

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 B

View File

@ -1,6 +0,0 @@
// This is a JavaScript module that is loaded on demand. It can export any number of
// functions, and may import other JavaScript modules if required.
export function showPrompt(message) {
return prompt(message, 'Type anything here');
}

View File

@ -11,8 +11,11 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNet.Mvc" Version="5.2.9" /> <PackageReference Include="Microsoft.AspNet.Mvc" Version="5.2.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.8" />
<PackageReference Include="Microsoft.Extensions.Caching.Redis" Version="2.2.0" /> <PackageReference Include="Redis.OM" Version="0.2.3" />
<PackageReference Include="Redis.OM" Version="0.2.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\BlueWest.Data.Auth\BlueWest.Data.Auth.csproj" /> <ProjectReference Include="..\BlueWest.Data.Auth\BlueWest.Data.Auth.csproj" />
@ -81,7 +84,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="Views\Jobs\Index.cshtml" /> <AdditionalFiles Include="Views\Data\Banks\Index.cshtml" />
<AdditionalFiles Include="Views\Data\Currencies\Index.cshtml" />
<AdditionalFiles Include="Views\Account\Index.cshtml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -90,6 +95,12 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</None> </None>
<Content Remove="appsettings.json" />
<None Include="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<Content Remove="appsettings.Development.json" />
<None Include="appsettings.Development.json" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -99,7 +110,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Pages" /> <Folder Include="wwwroot\static\profile" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,64 @@
using System.Diagnostics;
using System.Web.Mvc;
using BlueWest.Data.Auth.Context.Users;
using Microsoft.AspNetCore.Mvc;
using BlueWest.Views.Models;
using BlueWest.Views.Utils;
using Controller = Microsoft.AspNetCore.Mvc.Controller;
namespace BlueWest.Views.Controllers;
public class AccountController : UserController
{
private readonly ILogger<AccountController> _logger;
public AccountController(ApplicationUserManager userManager, ILogger<AccountController> logger) : base(userManager, logger)
{
_userManager = userManager;
_logger = logger;
}
public async Task<IActionResult> Index()
{
await OnEveryAction();
var user = await GetLoggedInUser();
return View(user);
}
public override void OnInitialization()
{
SetFooterMenu(LayoutCache.AccountRouteRecord.Children);
}
public async Task<IActionResult> Curriculums()
{
await OnEveryAction();
return View("Curriculums/Index");
}
public async Task<IActionResult> Change()
{
await OnEveryAction();
return View("ChangePassword");
}
[Microsoft.AspNetCore.Mvc.Route("[controller]/upload")]
public async Task<IActionResult> UploadCurriculum()
{
await OnEveryAction();
return View("Curriculums/Upload");
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task<IActionResult> Error()
{
return View(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier});
}
}

View File

@ -1,18 +1,96 @@
using BlueWest.Views.Utils; using System.Security.Claims;
using BlueWest.Data.Auth;
using BlueWest.Data.Auth.Context.Users;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Controller = Microsoft.AspNetCore.Mvc.Controller;
namespace BlueWest.Views.Controllers namespace BlueWest.Views.Controllers
{ {
[System.Web.Mvc.Route("/login")] public class AuthController : UserController
public class AuthController : Controller
{ {
private readonly IAuthManager _authManager;
public AuthController(ApplicationUserManager userManager, ILogger<AuthController> logger, IAuthManager authManager) : base(userManager, logger)
{
_userManager = userManager;
_logger = logger;
_authManager = authManager;
}
public IActionResult Index() public IActionResult Index()
{
OnEveryAction();
return View();
}
[AllowAnonymous]
[Microsoft.AspNetCore.Mvc.ActionName("LoginAction")]
public async Task<IActionResult> LoginAction(LoginRequest loginRequest)
{
var (success, sessionToken, identity) =
await _authManager.GetSessionTokenIdByLoginRequestViaCookie(loginRequest,
CookieAuthenticationDefaults.AuthenticationScheme);
if (!success) return Redirect(AuthLoginRoute);
if (success)
{
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(identity),
new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.Add(SessionConstants.DefaultSessionMaxAge)
});
HttpContext.Session.SetString("hello", "world");
return Redirect(RootLocation);
}
return Redirect(RootLocation);
}
public IActionResult Login()
{ {
this.HandleGlobalization(); this.HandleGlobalization();
return View(); return View();
} }
}
}
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Redirect("/");
}
public async Task<IActionResult> Signup()
{
await OnEveryAction();
return View();
}
[Microsoft.AspNetCore.Mvc.ActionName("SignupAction")]
public async Task<IActionResult> SignupAction(RegisterRequest registerRequest)
{
var result = await _authManager.CreateUserAsync(registerRequest);
if (result.Succeeded)
{
return RedirectToAction("Login");
}
return RedirectToAction("Signup");
}
}
}

View File

@ -1,17 +1,66 @@
using System.Web.Mvc;
using BlueWest.Data.Auth.Context.Users;
using BlueWest.Views.Utils; using BlueWest.Views.Utils;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Controller = Microsoft.AspNetCore.Mvc.Controller;
using Microsoft.AspNetCore.Http;
namespace BlueWest.Views.Controllers namespace BlueWest.Views.Controllers
{ {
[Authorize]
public class DataController : Controller
public class DataController : UserController
{ {
public IActionResult Index()
public DataController(ApplicationUserManager userManager, ILogger<UserController> logger) : base(userManager,
logger)
{ {
this.HandleGlobalization(); _userManager = userManager;
ViewData[FooterMenuId] = LayoutCache.DataRoute.Children; _logger = logger;
}
public async Task<IActionResult> Index()
{
await OnEveryAction();
return View(); return View();
} }
[ChildActionOnly]
public async Task<IActionResult> Countries()
{
await OnEveryAction();
return View("Countries/Index");
}
[ChildActionOnly]
public async Task<IActionResult> Companies()
{
await OnEveryAction();
return View("Companies/Index");
}
[ChildActionOnly]
public async Task<IActionResult> Banks()
{
await OnEveryAction();
return View("Companies/Index");
}
[ChildActionOnly]
public async Task<IActionResult> Currencies()
{
await OnEveryAction();
return View("Currencies/Index");
}
public override void OnInitialization()
{
SetFooterMenu(LayoutCache.DataRouteRecord.Children);
}
} }
} }

View File

@ -1,24 +1,38 @@
using Microsoft.AspNetCore.Mvc; using BlueWest.Data.Auth.Context.Users;
using Microsoft.AspNetCore.Mvc;
using BlueWest.Views.Utils; using BlueWest.Views.Utils;
using Duende.IdentityServer.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Controller = Microsoft.AspNetCore.Mvc.Controller; using Controller = Microsoft.AspNetCore.Mvc.Controller;
namespace BlueWest.Views.Controllers; namespace BlueWest.Views.Controllers;
[System.Web.Mvc.Route("/")] [System.Web.Mvc.Route("/")]
public class HomeController : Controller [System.Web.Mvc.Authorize]
public class HomeController : UserController
{ {
private readonly ILogger<HomeController> _logger; public HomeController(ApplicationUserManager userManager, ILogger<HomeController> logger) : base(userManager, logger)
public HomeController(ILogger<HomeController> logger, IOptions<RequestLocalizationOptions> options)
{ {
_userManager = userManager;
_logger = logger; _logger = logger;
} }
public IActionResult Index()
[AllowAnonymous]
public async Task<IActionResult> Index()
{ {
this.HandleGlobalization(); await OnEveryAction();
if (!User.IsAuthenticated())
{
return Redirect("/auth/login");
}
return View(); return View();
} }
} }

View File

@ -1,33 +0,0 @@
using System.Diagnostics;
using System.Web.Mvc;
using Microsoft.AspNetCore.Mvc;
using BlueWest.Views.Models;
using BlueWest.Views.Utils;
using Controller = Microsoft.AspNetCore.Mvc.Controller;
namespace BlueWest.Views.Controllers;
public class JobsController : Controller
{
private readonly ILogger<JobsController> _logger;
public JobsController(ILogger<JobsController> logger)
{
_logger = logger;
}
public IActionResult Index()
{
this.HandleGlobalization();
ViewData["Title"] = "Home Page";
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel {RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier});
}
}

View File

@ -1,29 +1,49 @@
using BlueWest.Data.Auth.Context.Users;
using BlueWest.Views.Utils; using BlueWest.Views.Utils;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace BlueWest.Views.Controllers namespace BlueWest.Views.Controllers
{ {
public class SystemController : Controller public class SystemController : UserController
{ {
private readonly ILogger<SystemController> _logger; public SystemController(ApplicationUserManager userManager, ILogger<SystemController> logger) : base(userManager, logger)
public SystemController(ILogger<SystemController> logger)
{ {
_logger = logger; _logger = logger;
_userManager = userManager;
} }
public async Task<IActionResult> Index()
public IActionResult Index()
{ {
this.HandleGlobalization();
ViewData[FooterMenuId] = LayoutCache.SystemRoute.Children; await OnEveryAction();
return View();
}
public override void OnInitialization()
{
SetFooterMenu(LayoutCache.SystemRoute.Children);
}
public async Task<IActionResult> Users()
{
await OnEveryAction();
return View(); return View();
} }
public IActionResult Users() public async Task<IActionResult> Logs()
{ {
await OnEveryAction();
return View(); return View();
} }
public async Task<IActionResult> Roles()
{
await OnEveryAction();
return View("Roles/Index");
}
} }
} }

View File

@ -0,0 +1,112 @@
using System.Net;
using System.Net.Sockets;
using BlueWest.Data.Application.Users;
using BlueWest.Data.Auth.Context.Users;
using BlueWest.Views.Models;
using BlueWest.Views.Utils;
using Duende.IdentityServer.Extensions;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
namespace BlueWest.Views.Controllers;
public class UserController : Controller
{
protected ILogger<UserController> _logger;
protected ApplicationUserManager _userManager;
private List<RouteRecord> _footerMenu;
public UserController(ApplicationUserManager userManager, ILogger<UserController> logger)
{
_logger = logger;
_userManager = userManager;
_footerMenu = new List<RouteRecord>();
}
public async Task OnEveryAction()
{
HandleGlobalization();
SetFooterMenuViewData();
await SetUserProfileViewData();
OnInitialization();
SetFooterMenuViewData();
}
public virtual void OnInitialization()
{
}
protected void SetFooterMenuViewData()
{
ViewData[FooterMenuViewDataId] = _footerMenu;
}
public void SetFooterMenu(List<RouteRecord> routeRecords)
{
_footerMenu = routeRecords;
}
public async Task SetUserProfileViewData()
{
if (!ViewData.ContainsKey(UserViewDataId))
{
ViewData[UserViewDataId] = await GetLoggedInUser();
}
}
public async Task<ApplicationUserUnique> GetLoggedInUser()
{
if (User.IsAuthenticated())
{
ApplicationUser? user = await _userManager.GetUserAsync(User);
return new ApplicationUserUnique(user);
}
return null;
}
public IpInformation ExtractIpInformation(IPAddress? ipAddress)
{
string ipAddressString = "";
IpType ipType = IpType.Unknown;
if (ipAddress != null)
{
if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
{
ipType = IpType.Ipv4;
ipAddressString = ipAddress.MapToIPv4().ToString();
}
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
{
ipType = IpType.Ipv6;
ipAddressString = ipAddress
.MapToIPv6()
.ToString();
}
}
return new IpInformation(ipType, ipAddressString);
}
public void HandleGlobalization()
{
var requestCultureFeature = HttpContext.Features.Get<IRequestCultureFeature>();
string currentCultureName = DefaultCultureName;
if (requestCultureFeature != null)
{
currentCultureName = requestCultureFeature.RequestCulture.Culture.Name;
}
ViewData[LanguageViewStorage] = currentCultureName;
IPAddress? remoteIpAddress = Request.HttpContext.Connection.RemoteIpAddress;
IpInformation ipInformation = ExtractIpInformation(Request.HttpContext.Connection.RemoteIpAddress);
ViewData[IpViewStorage] = ipInformation;
}
}

View File

@ -0,0 +1,138 @@
namespace BlueWest.Views.Languages;
public static class SiteContent
{
internal static readonly Dictionary<string, Dictionary<string, string>> RouteTitle =
new Dictionary<string, Dictionary<string, string>>
{
{
RootKeyName, new Dictionary<string, string>()
{
{"pt", "Inicio"},
{"eng", "Home"},
{"en-gb", "Home"}
}
},
{
SystemKeyName, new Dictionary<string, string>()
{
{"pt", "Sistema"},
{"eng", "System"},
{"en-gb", "System"},
}
},
{
DataKeyName, new Dictionary<string, string>()
{
{"pt", "Dados"},
{"eng", "Data"},
{"en-gb", "Data"}
}
},
{
RolesKeyName, new Dictionary<string, string>()
{
{"pt", "Tipos de Utilizador"},
{"en-gb", "Roles"},
{"eng", "Roles"}
}
},
{
ApplicationUsersKeyName, new Dictionary<string, string>()
{
{"pt", "Utilizadores da Aplicação"},
{"en-gb", "Users"},
{"eng", "Users"}
}
},
{
LogsKeyName, new Dictionary<string, string>()
{
{"pt", "Logs"},
{"en-gb", "logs"},
{"eng", "logs"}
}
},
{
SettingsKeyName, new Dictionary<string, string>()
{
{"pt", "Personalização"},
{"eng", "Settings"},
{"en-gb", "Settings"},
}
},
{
CompaniesKeyName, new Dictionary<string, string>()
{
{"pt", "Empresas"},
{"eng", "Companies"},
{"en-gb", "Companies"},
}
},
{
IndustriesKeyName, new Dictionary<string, string>()
{
{"pt", "Indústrias"},
{"en-gb", "Industries"},
{"eng", "Industries"},
}
},
{
CurrenciesKeyName, new Dictionary<string, string>()
{
{"pt", "Moedas"},
{"en-gb", "Currencies"},
{"eng", "Currencies"},
}
},
{
CountriesKeyName, new Dictionary<string, string>()
{
{"pt", "Países"},
{"eng", "Countries"},
{"en-gb", "Countries"}
}
},
{
BanksKeyName, new Dictionary<string, string>()
{
{"pt", "Bancos"},
{"eng", "Banks"},
{"en-gb", "Banks"},
}
},
{
DataUsersKeyName, new Dictionary<string, string>()
{
{"pt", "Utilizadores"},
{"eng", "Users"},
{"en-gb", "Users"},
}
},
{
AccountKeyName, new Dictionary<string, string>()
{
{"pt", "Perfil"},
{"eng", "Account"},
{"en-gb", "Account"},
}
},
{
ChangePasswordKeyName, new Dictionary<string, string>()
{
{"pt", "Alterar Senha"},
{"eng", "Change Password"},
{"en-gb", "Change Password"},
}
},
};
}

View File

@ -0,0 +1,7 @@
namespace BlueWest.Views.Models;
public record IpInformation
(
IpType IpType,
string Address
);

View File

@ -0,0 +1,6 @@
namespace BlueWest.Views.Models;
public enum IpType
{
Ipv6, Ipv4, Unknown
}

View File

@ -24,7 +24,7 @@ namespace BlueWest.WebApi
MainHost = CreateHostBuilder(args) MainHost = CreateHostBuilder(args)
.UseContentRoot(Directory.GetCurrentDirectory()) .UseContentRoot(Directory.GetCurrentDirectory())
.Build(); .Build();
MainHost.Run(); MainHost.Run();

View File

@ -11,7 +11,7 @@
"BlueWest.Views": { "BlueWest.Views": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": false,
"applicationUrl": "https://localhost:7022;http://localhost:5204", "applicationUrl": "https://localhost:7022;http://localhost:5204",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"

View File

@ -42,7 +42,8 @@ public sealed class SessionManager : ISessionCache
/// <returns></returns> /// <returns></returns>
public async Task<SessionToken> GetSessionTokenByIdAsync(string tokenId) public async Task<SessionToken> GetSessionTokenByIdAsync(string tokenId)
{ {
return await _sessionTokens.Where(x => x.Id == tokenId) return
await _sessionTokens.Where(x => x.Id == tokenId)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
} }

View File

@ -30,10 +30,6 @@ public class Startup
services.AddSingleton<LayoutCache>(); services.AddSingleton<LayoutCache>();
// Add services to the container. // Add services to the container.
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddJsonFile("config.json")
.Build();
services.Configure<CookiePolicyOptions>(options => services.Configure<CookiePolicyOptions>(options =>
{ {
// This lambda determines whether user consent for non-essential cookies is needed for a given request. // This lambda determines whether user consent for non-essential cookies is needed for a given request.
@ -45,19 +41,12 @@ public class Startup
{ {
builder.AddSimpleConsole(); builder.AddSimpleConsole();
}); });
services.AddSession(options =>
{
options.Cookie.Domain = SessionConstants.CookieDomain;
options.Cookie.HttpOnly = true;
});
services.AddControllersWithViews(x => x.EnableEndpointRouting = false); services.AddControllersWithViews(x => x.EnableEndpointRouting = false);
services.AddSession(options => options.IdleTimeout = TimeSpan.FromHours(8));
services.AddAuthServerServices(_configuration, _environment);
services.PrepareMySqlDatabasePool(_configuration, _environment);
services.AddAuthServerServices(_configuration, _environment, configuration);
services.PrepareMySqlDatabasePool(_configuration, _environment, configuration);
} }
@ -93,6 +82,7 @@ public class Startup
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseSession(); app.UseSession();
app.UseMvcWithDefaultRoute(); app.UseMvcWithDefaultRoute();

View File

@ -4,7 +4,7 @@ using BlueWest.Data.Application.Users;
using BlueWest.Data.Auth; using BlueWest.Data.Auth;
using BlueWest.Data.Auth.Context.Users; using BlueWest.Data.Auth.Context.Users;
using BlueWest.Domain; using BlueWest.Domain;
using BlueWest.WebApi.Configuration; using BlueWest.Views.Utils;
using BlueWest.WebApi.Context.Users; using BlueWest.WebApi.Context.Users;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
@ -17,142 +17,127 @@ namespace BlueWest.Views;
public static class StartupExtensions public static class StartupExtensions
{ {
private static MySqlServerVersion GetMySqlServerVersion(int major, int minor, int build) => new (new Version(major, minor, build)); private static MySqlServerVersion GetMySqlServerVersion(int major, int minor, int build) =>
new(new Version(major, minor, build));
private static string GetConnectionString(this IConfiguration configurationRoot, string db)
private static BlueWestConnectionString GetConnectionString(this IConfigurationRoot configurationRoot)
{ {
// Docker / No-Docker
var startupMode = configurationRoot["mode"]; var startupMode = configurationRoot["mode"];
if (startupMode == "docker") if (!string.IsNullOrEmpty(startupMode) && startupMode == "docker")
{ {
var config = configurationRoot.Get<ConnectionStringDocker>(); var config = configurationRoot.GetSection("ConnectionStringDocker")[db];
return config; return config;
} }
else else
{ {
var config = configurationRoot.Get<ConnectionStringNoDocker>(); return configurationRoot.GetSection("ConnectionStringNoDocker")[db] ?? string.Empty;
return config;
} }
return null;
} }
internal static IServiceCollection AddAuthServerServices(this IServiceCollection services, IConfiguration configuration , IWebHostEnvironment environment, IConfigurationRoot configurationRoot)
private static bool IsDockerMode(this IConfiguration config)
{
var startupMode = config["mode"];
return startupMode == "docker";
}
internal static IServiceCollection AddAuthServerServices(this IServiceCollection services,
IConfiguration configuration, IWebHostEnvironment environment)
{
var connectionString = configuration.GetConnectionString("Redis");
if (string.IsNullOrEmpty(connectionString))
{ {
throw new InvalidOperationException("Redis connection string is empty");
}
services.AddSession(options =>
{
options.Cookie.Domain = SessionConstants.CookieDomain;
options.Cookie.HttpOnly = true;
options.IdleTimeout = TimeSpan.FromHours(8);
});
var connectionString = configurationRoot.GetConnectionString(); services
.AddSingleton(new RedisConnectionProvider(connectionString))
.AddScoped<IJwtTokenHandler, JwtTokenHandler>()
.AddScoped<IJwtFactory, JwtFactory>()
.AddHostedService<SessionManager>()
.AddSingleton<ISessionCache, SessionManager>()
.AddScoped<UserRepository>()
.AddScoped<IUserManager, ApplicationUserManager>()
.AddScoped<IAuthManager, AuthManager>()
.AddScoped<IHasher, Hasher>();
if (connectionString == null) // Database Context and Swagger
// Register the ConfigurationBuilder instance of AuthSettings
var authSettings = configuration.GetSection(nameof(AuthSettings));
services.Configure<AuthSettings>(authSettings);
var signingKey = new SymmetricSecurityKey
(Encoding.ASCII.GetBytes(authSettings[nameof(AuthSettings.SecretKey)]));
// jwt wire up
// Get options from app settings
var jwtAppSettingOptions = configuration
.GetSection(nameof(JwtIssuerOptions));
// Configure JwtIssuerOptions
services.Configure<JwtIssuerOptions>(options =>
{
options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
options.SigningCredentials = new SigningCredentials
(signingKey, SecurityAlgorithms.HmacSha256);
});
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],
ValidateAudience = true,
ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)],
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
RequireExpirationTime = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
services.AddAuthentication(options =>
{ {
throw new InvalidOperationException("Redis connection string is empty"); options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
} options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
services.AddDistributedRedisCache(options => options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{ {
options.Configuration = connectionString.Redis; options.LoginPath = Routes.AuthLoginRoute;
options.LogoutPath = Routes.AuthLogoutRoute;
}); });
services
.AddSingleton(new RedisConnectionProvider(connectionString.Redis))
.AddScoped<IJwtTokenHandler, JwtTokenHandler>()
.AddScoped<IJwtFactory, JwtFactory>()
.AddHostedService<SessionManager>()
.AddSingleton<ISessionCache, SessionManager>()
.AddScoped<UserRepository>()
.AddScoped<IUserManager, ApplicationUserManager>()
.AddScoped<IAuthManager, AuthManager>()
.AddScoped<IHasher, Hasher>();
// Database Context and Swagger
// api user claim policy
services.AddAuthorization(options =>
{
options.AddPolicy(SessionConstants.ApiNamePolicy,
policy => policy.RequireClaim(Data.Auth.Context.Users.Constants.JwtClaimIdentifiers.Rol,
Data.Auth.Context.Users.Constants.JwtClaims.ApiAccess));
});
// Register the ConfigurationBuilder instance of AuthSettings // add identity
var authSettings = configuration.GetSection(nameof(AuthSettings)); var identityBuilder = services.AddIdentityCore<ApplicationUser>(o =>
services.Configure<AuthSettings>(authSettings);
var signingKey = new SymmetricSecurityKey
(Encoding.ASCII.GetBytes(authSettings[nameof(AuthSettings.SecretKey)]));
// jwt wire up
// Get options from app settings
var jwtAppSettingOptions = configuration
.GetSection(nameof(JwtIssuerOptions));
// Configure JwtIssuerOptions
services.Configure<JwtIssuerOptions>(options =>
{
options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
options.SigningCredentials = new SigningCredentials
(signingKey, SecurityAlgorithms.HmacSha256);
});
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],
ValidateAudience = true,
ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)],
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
RequireExpirationTime = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/";
options.LogoutPath = "/logout";
})
.AddJwtBearer(configureOptions =>
{
configureOptions.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
configureOptions.TokenValidationParameters = tokenValidationParameters;
configureOptions.SaveToken = true;
configureOptions.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
},
};
});
// api user claim policy
services.AddAuthorization(options =>
{
options.AddPolicy(SessionConstants.ApiNamePolicy,
policy => policy.RequireClaim(Data.Auth.Context.Users.Constants.JwtClaimIdentifiers.Rol,
Data.Auth.Context.Users.Constants.JwtClaims.ApiAccess));
});
// add identity
var identityBuilder = services.AddIdentityCore<ApplicationUser>(o =>
{ {
o.User.RequireUniqueEmail = true; o.User.RequireUniqueEmail = true;
// configure identity options // configure identity options
o.Password.RequireDigit = false; o.Password.RequireDigit = false;
o.Password.RequireLowercase = false; o.Password.RequireLowercase = false;
@ -160,87 +145,82 @@ public static class StartupExtensions
o.Password.RequireNonAlphanumeric = false; o.Password.RequireNonAlphanumeric = false;
o.Password.RequiredLength = 6; o.Password.RequiredLength = 6;
}) })
.AddUserManager<ApplicationUserManager>() .AddUserManager<ApplicationUserManager>()
.AddUserStore<UserRepository>(); .AddUserStore<UserRepository>();
identityBuilder = new IdentityBuilder(identityBuilder.UserType, typeof(ApplicationRole), identityBuilder.Services); identityBuilder =
identityBuilder new IdentityBuilder(identityBuilder.UserType, typeof(ApplicationRole), identityBuilder.Services);
.AddEntityFrameworkStores<ApplicationUserDbContext>() identityBuilder
.AddDefaultTokenProviders(); .AddEntityFrameworkStores<ApplicationUserDbContext>()
.AddDefaultTokenProviders();
return services; return services;
}
/// <summary>
/// Get MYSQL Connection String
/// </summary>
/// <param name="optionsBuilder"></param>
/// <param name="configuration"></param>
/// <param name="environment"></param>
private static DbContextOptionsBuilder GetMySqlSettings(
this DbContextOptionsBuilder optionsBuilder,
IConfiguration configuration,
IWebHostEnvironment environment)
{
var sqlVersion = GetMySqlServerVersion(8, 0, 11);
// Docker / No-Docker
var mySqlConnectionString = configuration.GetConnectionString("MySQL");
if (mySqlConnectionString == string.Empty)
{
throw new InvalidOperationException("MySQL Connection string appears to be empty.");
} }
optionsBuilder
/// <summary> .UseMySql(
/// Get MYSQL Connection String mySqlConnectionString,
/// </summary> sqlVersion)
/// <param name="optionsBuilder"></param> .UseMySql(sqlVersion,
/// <param name="configuration"></param> builder => { builder.EnableRetryOnFailure(6, TimeSpan.FromSeconds(3), null); });
/// <param name="environment"></param>
private static DbContextOptionsBuilder GetMySqlSettings(
this DbContextOptionsBuilder optionsBuilder,
IConfiguration configuration,
IConfigurationRoot configurationRoot,
IWebHostEnvironment environment)
{
var sqlVersion = GetMySqlServerVersion(8, 0, 11);
// Docker / No-Docker
string mySqlConnectionString = configurationRoot.GetConnectionString().MySql;
if (mySqlConnectionString == string.Empty)
{
throw new InvalidOperationException("Fatal error: MySQL Connection string is empty.");
}
optionsBuilder // The following three options help with debugging, but should
.UseMySql( // be changed or removed for production.
mySqlConnectionString, if (environment.IsDevelopment())
sqlVersion) {
.UseMySql(sqlVersion, optionsBuilder
builder => .LogTo(Console.WriteLine, LogLevel.Information)
{ .EnableSensitiveDataLogging()
builder.EnableRetryOnFailure(6, TimeSpan.FromSeconds(3), null); .EnableDetailedErrors();
}); }
// The following three options help with debugging, but should
// be changed or removed for production.
if (environment.IsDevelopment())
{
optionsBuilder
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
return optionsBuilder;
}
/// <summary>
/// Setup database Contexts
/// </summary>
/// <param name="serviceCollection"></param>
/// <param name="configuration"></param>
/// <param name="environment"></param>
/// <returns></returns>
public static IServiceCollection PrepareMySqlDatabasePool(this IServiceCollection serviceCollection,
IConfiguration configuration, IWebHostEnvironment environment, IConfigurationRoot configurationRoot)
{
return serviceCollection
.AddDbContextPool<UserDbContext>(options =>
options.GetMySqlSettings(configuration, configurationRoot, environment))
.AddDbContextPool<CountryDbContext>(options =>
options.GetMySqlSettings(configuration, configurationRoot, environment))
.AddDbContextPool<FinanceDbContext>(options =>
options.GetMySqlSettings(configuration, configurationRoot, environment))
.AddDbContextPool<CompanyDbContext>(options =>
options.GetMySqlSettings(configuration, configurationRoot, environment))
.AddDbContextPool<ApplicationUserDbContext>(options =>
options.GetMySqlSettings(configuration, configurationRoot, environment));
}
return optionsBuilder;
}
/// <summary>
/// Setup database Contexts
/// </summary>
/// <param name="serviceCollection"></param>
/// <param name="configuration"></param>
/// <param name="environment"></param>
/// <returns></returns>
public static IServiceCollection PrepareMySqlDatabasePool(this IServiceCollection serviceCollection,
IConfiguration configuration, IWebHostEnvironment environment)
{
return serviceCollection
.AddDbContextPool<UserDbContext>(options =>
options.GetMySqlSettings(configuration, environment))
.AddDbContextPool<CountryDbContext>(options =>
options.GetMySqlSettings(configuration, environment))
.AddDbContextPool<FinanceDbContext>(options =>
options.GetMySqlSettings(configuration, environment))
.AddDbContextPool<CompanyDbContext>(options =>
options.GetMySqlSettings(configuration, environment))
.AddDbContextPool<ApplicationUserDbContext>(options =>
options.GetMySqlSettings(configuration, environment));
}
} }

View File

@ -1,27 +0,0 @@
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
namespace BlueWest.Views.Utils;
public static class ControllerExtensions
{
public static void HandleGlobalization(this Controller controller)
{
var context = controller.HttpContext;
var requestCultureFeature = context.Features.Get<IRequestCultureFeature>();
var currentCultureName = Routes.DefaultCultureName;
if (requestCultureFeature != null)
{
currentCultureName = requestCultureFeature.RequestCulture.Culture.Name;
}
controller.ViewData[LanguageViewStorage] = currentCultureName;
}
public static void HandlePageName(this Controller controller, string location)
{
}
}

View File

@ -0,0 +1,34 @@
using BlueWest.Data.Application.Users;
using BlueWest.Data.Auth;
using BlueWest.Views.Models;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BlueWest.Views.Utils;
public static class DataUtil
{
public static ApplicationUserUnique GetUserViewData(this ViewDataDictionary viewData)
{
if (viewData[UserViewDataId] is ApplicationUserUnique user)
{
return user;
}
return null!;
}
public static IpInformation GetIpInformation(this ViewDataDictionary viewData)
{
if (viewData[IpViewStorage] is IpInformation ipInformation)
{
return ipInformation;
}
return null!;
}
public static string GetRootUrl(this ViewDataDictionary viewData)
{
return SessionConstants.CookieDomain;
}
}

View File

@ -1,5 +1,7 @@
using System.Diagnostics;
using BlueWest.Views.Controllers; using BlueWest.Views.Controllers;
using BlueWest.Views.Controllers.Data; using BlueWest.Views.Controllers.Data;
using BlueWest.Views.Languages;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BlueWest.Views.Utils; namespace BlueWest.Views.Utils;
@ -8,6 +10,12 @@ internal class LayoutCache
{ {
#region Route Tree #region Route Tree
private static readonly RouteRecord NonLoggedInRoot = new RouteRecord(
AuthLoginRoute,
AuthLoginKeyName,
nameof(AuthController), new List<RouteRecord>());
private static readonly RouteRecord Root = new RouteRecord( private static readonly RouteRecord Root = new RouteRecord(
RootKeyName, RootKeyName,
RootLocation, RootLocation,
@ -82,11 +90,14 @@ internal class LayoutCache
ViewType.System ViewType.System
), ),
new RouteRecord( new RouteRecord(
JobsKeyName, AccountKeyName,
JobsRouteLocation, AccountRouteLocation,
nameof(JobsController), nameof(AccountController),
new List<RouteRecord>(), new List<RouteRecord>()
ViewType.Jobs {
new RouteRecord(ChangePasswordKeyName, ChangePasswordRouteLocation, nameof(AccountController), new List<RouteRecord>())
},
ViewType.Account
), ),
}, ViewType.Root); }, ViewType.Root);
@ -97,14 +108,11 @@ internal class LayoutCache
internal static readonly RouteRecord SystemRoute = internal static readonly RouteRecord SystemRoute =
Root.Children.FirstOrDefault(x => x.ViewType == ViewType.System)!; Root.Children.FirstOrDefault(x => x.ViewType == ViewType.System)!;
internal static readonly RouteRecord DataRoute = internal static readonly RouteRecord DataRouteRecord =
Root.Children.FirstOrDefault(x => x.ViewType == ViewType.Data)!; Root.Children.FirstOrDefault(x => x.ViewType == ViewType.Data)!;
internal static readonly RouteRecord JobRoute = internal static readonly RouteRecord AccountRouteRecord =
Root.Children.FirstOrDefault(x => x.ViewType == ViewType.Jobs)!; Root.Children.FirstOrDefault(x => x.ViewType == ViewType.Account)!;
internal static readonly RouteRecord ProfileRoute =
Root.Children.FirstOrDefault(x => x.ViewType == ViewType.Profile)!;
#endregion Routing Utils #endregion Routing Utils
@ -112,13 +120,13 @@ internal class LayoutCache
internal static List<RouteView> GetDefaultFooterMenu(ViewDataDictionary dictionary) internal static List<RouteView> GetDefaultFooterMenu(ViewDataDictionary dictionary)
{ {
var location = GetLocation(dictionary); var location = GetUserLanguage(dictionary);
var menu = LayoutCache var menu = LayoutCache
.Root .Root
.Children; .Children;
if (dictionary[FooterMenuId] is List<RouteRecord> footerMenu) if (dictionary[FooterMenuViewDataId] is List<RouteRecord> footerMenu)
{ {
menu = footerMenu; menu = footerMenu;
} }
@ -126,12 +134,12 @@ internal class LayoutCache
return menu return menu
.Select(x => .Select(x =>
{ {
if (Translation[x.routeKey].ContainsKey(location)) if (SiteContent.RouteTitle[x.routeKey].ContainsKey(location))
{ {
return new RouteView(Translation[x.routeKey][location], return new RouteView(SiteContent.RouteTitle[x.routeKey][location],
location); location);
} }
return new RouteView(Translation[x.routeKey][DefaultCultureName], return new RouteView(SiteContent.RouteTitle[x.routeKey][DefaultCultureName],
x.location); x.location);
}) })
@ -140,9 +148,16 @@ internal class LayoutCache
} }
internal static List<RouteView> GetDefaultHeaderMenu(ViewDataDictionary dictionary) internal static List<RouteView> GetDefaultHeaderMenu(ViewDataDictionary dictionary, bool userAuthenticated = false)
{ {
var location = GetLocation(dictionary); if (!userAuthenticated)
{
var menuToShow = new List<RouteView>();
menuToShow.Add(new RouteView("Blue West", "/"));
return menuToShow;
}
var location = GetUserLanguage(dictionary);
var menu = LayoutCache var menu = LayoutCache
.Root .Root
@ -156,20 +171,27 @@ internal class LayoutCache
return menu return menu
.Select(x => .Select(x =>
{ {
if (Translation[x.routeKey].ContainsKey(location)) if (SiteContent.RouteTitle[x.routeKey].ContainsKey(location))
{ {
return new RouteView(Translation[x.routeKey][location], return new RouteView(SiteContent.RouteTitle[x.routeKey][location],
x.Location); x.Location);
} }
return new RouteView(Translation[x.routeKey][DefaultCultureName], return new RouteView(SiteContent.RouteTitle[x.routeKey][DefaultCultureName],
x.location); x.location);
}) })
.ToList(); .ToList();
} }
internal static string GetLocation(ViewDataDictionary dictionary)
[Conditional("DEBUG")]
internal static void IsDevMode(ref bool isDebug)
{
isDebug = true;
}
internal static string GetUserLanguage(ViewDataDictionary dictionary)
{ {
return (dictionary[LanguageViewStorage] as string)!; return (dictionary[LanguageViewStorage] as string)!;
} }

View File

@ -6,185 +6,87 @@ namespace BlueWest.Views.Utils
{ {
public const string DefaultCultureName = "en-gb"; public const string DefaultCultureName = "en-gb";
#region Layout Keys #region View Data Keys
internal const string FooterMenuId = "m1"; internal const string FooterMenuViewDataId = "m1";
internal const string UserViewDataId = "uvd";
internal const string HeaderMenuId = "m2"; internal const string HeaderMenuId = "m2";
internal const string LanguageViewStorage = "i80"; internal const string LanguageViewStorage = "i81";
internal const string IpViewStorage = "ip";
internal const string AgentViewStorage = "ag2";
#endregion Layout Keys
#endregion View Data Keys
#region Route Keys
internal const string DataKeyName = "data";
internal const string RootKeyName = "root"; internal const string RootKeyName = "root";
internal const string RootLocation = "/"; internal const string RootLocation = "/";
internal const string SystemKeyName = "system";
internal const string RootAuthRoute = "/auth";
internal const string RootAuthKeyName = "auth";
internal const string AuthLoginRoute = $"{RootAuthRoute}/login";
internal const string AuthLoginKeyName = "login";
internal const string AuthSignupRoute = $"{RootAuthRoute}/signup";
internal const string AuthSignupKeyName = "signup";
internal const string RolesLocation = $"{SystemRouteLocation}/roles";
internal const string RolesKeyName = "roles"; internal const string RolesKeyName = "roles";
internal const string CompaniesKeyName = "companies";
internal const string CountriesKeyName = "countries"; internal const string ApplicationUsersLocation = $"{SystemRouteLocation}/users";
internal const string CurrenciesKeyName = "currencies";
internal const string IndustriesKeyName = "industries";
internal const string DataUsersKeyName = "users";
internal const string BanksKeyName = "banks";
internal const string JobsKeyName = "jobs";
internal const string SettingsKeyName = "settings";
internal const string LogsKeyName = "logs";
internal const string ApplicationUsersKeyName = "app_users"; internal const string ApplicationUsersKeyName = "app_users";
#endregion Route Keys
// Translation Database Keys
#region Routes Data
// Routes
internal const string RolesLocation = $"{SystemRouteLocation}/roles";
internal const string ApplicationUsersLocation = $"{SystemRouteLocation}/users";
internal const string LogsLocation = $"{SystemRouteLocation}/logs"; internal const string LogsLocation = $"{SystemRouteLocation}/logs";
internal const string LogsKeyName = "logs";
internal const string SettingsRouteLocation = $"{SystemRouteLocation}/settings"; internal const string SettingsRouteLocation = $"{SystemRouteLocation}/settings";
internal const string SettingsKeyName = "settings";
internal const string DataLocation = $"/data"; internal const string DataLocation = $"/data";
internal const string DataKeyName = "data";
// Banks
// Data users
internal const string DataUsersLocation = "/data/users"; internal const string DataUsersLocation = "/data/users";
internal const string JobsRouteLocation = "/jobs"; internal const string DataUsersKeyName = "users";
internal const string SystemRouteLocation = $"/system"; internal const string SystemRouteLocation = $"/system";
internal const string SystemKeyName = "system";
internal const string BanksKeyName = "banks";
internal const string BanksLocation = $"{DataLocation}/banks"; internal const string BanksLocation = $"{DataLocation}/banks";
internal const string CountriesLocation = $"{DataLocation}/countries"; internal const string CountriesLocation = $"{DataLocation}/countries";
internal const string CountriesKeyName = "countries";
internal const string CurrenciesLocation = $"{DataLocation}/currencies"; internal const string CurrenciesLocation = $"{DataLocation}/currencies";
internal const string CurrenciesKeyName = "currencies";
internal const string CompaniesLocation = $"{DataLocation}/companies"; internal const string IndustriesKeyName = "industries";
internal const string IndustriesLocation = $"{DataLocation}/industries"; internal const string IndustriesLocation = $"{DataLocation}/industries";
internal const string CompaniesLocation = $"{DataLocation}/companies";
internal const string CompaniesKeyName = "companies";
// Jobs
internal const string Curriculums = "curriculums";
internal const string Proposals = "proposals";
internal const string Offers = "offers";
internal const string Processes = "processes";
internal const string AuthLogoutRoute = "/auth/logout";
// Account
internal const string AccountRouteLocation = "/account";
internal const string AccountKeyName = "profile";
internal const string ChangePasswordKeyName = "changepwd";
internal const string ChangePasswordRouteLocation = $"{AccountRouteLocation}/change";
#endregion Routes Data
internal static Dictionary<string, Dictionary<string, string>> Translation =
new Dictionary<string, Dictionary<string, string>>
{
{
RootKeyName, new Dictionary<string, string>()
{
{"pt", "Inicio"},
{"eng", "Home"},
{"en-gb", "Home"}
}
},
{
SystemKeyName, new Dictionary<string, string>()
{
{"pt", "Sistema"},
{"eng", "System"},
{"en-gb", "System"},
}
},
{
DataKeyName, new Dictionary<string, string>()
{
{"pt", "Dados"},
{"eng", "Data"},
{"en-gb", "Data"}
}
},
{
RolesKeyName, new Dictionary<string, string>()
{
{"pt", "Tipos de Utilizador"},
{"en-gb", "Roles"},
{"eng", "Roles"}
}
},
{
ApplicationUsersKeyName, new Dictionary<string, string>()
{
{"pt", "Utilizadores da Aplicação"},
{"en-gb", "Users"},
{"eng", "Users"}
}
},
{
LogsKeyName, new Dictionary<string, string>()
{
{"pt", "Logs"},
{"en-gb", "logs"},
{"eng", "logs"}
}
},
{
SettingsKeyName, new Dictionary<string, string>()
{
{"pt", "Personalização"},
{"eng", "Settings"},
{"en-gb", "Settings"},
}
},
{
CompaniesKeyName, new Dictionary<string, string>()
{
{"pt", "Empresas"},
{"eng", "Companies"},
{"en-gb", "Companies"},
}
},
{
IndustriesKeyName, new Dictionary<string, string>()
{
{"pt", "Indústrias"},
{"en-gb", "Industries"},
{"eng", "Industries"},
}
},
{
CurrenciesKeyName, new Dictionary<string, string>()
{
{"pt", "Moedas"},
{"en-gb", "Currencies"},
{"eng", "Currencies"},
}
},
{
CountriesKeyName, new Dictionary<string, string>()
{
{"pt", "Países"},
{"eng", "Countries"},
{"en-gb", "Countries"}
}
},
{
BanksKeyName, new Dictionary<string, string>()
{
{"pt", "Bancos"},
{"eng", "Banks"},
{"en-gb", "Banks"},
}
},
{
DataUsersKeyName, new Dictionary<string, string>()
{
{"pt", "Utilizadores"},
{"eng", "Users"},
{"en-gb", "Users"},
}
},
{
JobsKeyName, new Dictionary<string, string>()
{
{"pt", "Anúncios de trabalho"},
{"eng", "Jobs"},
{"en-gb", "Jobs"},
}
},
};
} }
} }

View File

@ -6,8 +6,7 @@ namespace BlueWest.Views
{ {
System, System,
Data, Data,
Jobs, Account,
Profile,
Root, Root,
Undefined Undefined

View File

@ -0,0 +1,111 @@
@using BlueWest.Views.Utils
@model BlueWest.Data.Application.Users.ApplicationUserUnique
@{
var user = @Model;
}
<div class="q-ma-lg">
<form method="POST" action="EditProfile" class="q-form q-gutter-md">
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap" tabindex="-1">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" tabindex="0" aria-label="Phone Number" name="name" type="name" value="@Model.Id" disabled="disabled">
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Id</div>
</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" name="email" aria-label="Email" type="email" value="@user.Email">
</div>
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Email address</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap" tabindex="-1">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" tabindex="0" aria-label="Phone Number" name="name" type="name" value="@Model.PhoneNumber">
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Phone Number</div>
</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap" tabindex="-1">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" tabindex="0" aria-label="Phone Number" name="name" type="name" value="@Model.NormalizedEmail" disabled="disabled">
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Normalized Email</div>
</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap" tabindex="-1">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" tabindex="0" aria-label="Phone Number" name="name" type="name" value="@Model.UserName">
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Username</div>
</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap" tabindex="-1">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" tabindex="0" aria-label="Password Hash" name="name" type="name" value="@Model.PasswordHash" disabled="disabled">
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Username</div>
</div>
</div>
</div>
</div>
</label>
<div>
<button class="q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-primary text-white q-btn--actionable q-focusable q-hoverable">
<span class="q-focus-helper"></span>
<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
<span class="block">Save</span>
</span>
</button>
<button class="q-btn q-btn-item non-selectable no-outline q-btn--flat q-btn--rectangle text-primary q-btn--actionable q-focusable q-hoverable q-ml-sm" type="reset">
<span class="q-focus-helper"></span>
<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
<span class="block">Reset</span>
</span>
</button>
</div>
</form>
</div>
@await Html.PartialAsync("_FooterMenu")

View File

@ -0,0 +1,10 @@
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Curriculums Module</h1>
</div>
@Html.Partial("_FooterMenu");

View File

@ -0,0 +1,111 @@
@using BlueWest.Views.Utils
@model BlueWest.Data.Application.Users.ApplicationUserUnique
@{
var user = @Model;
}
<div class="q-ma-lg">
<form method="POST" action="EditProfile" class="q-form q-gutter-md">
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap" tabindex="-1">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" tabindex="0" aria-label="Phone Number" name="name" type="name" value="@Model.Id" disabled="disabled">
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Id</div>
</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" name="email" aria-label="Email" type="email" value="@user.Email">
</div>
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Email address</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap" tabindex="-1">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" tabindex="0" aria-label="Phone Number" name="name" type="name" value="@Model.PhoneNumber">
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Phone Number</div>
</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap" tabindex="-1">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" tabindex="0" aria-label="Phone Number" name="name" type="name" value="@Model.NormalizedEmail" disabled="disabled">
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Normalized Email</div>
</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap" tabindex="-1">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" tabindex="0" aria-label="Phone Number" name="name" type="name" value="@Model.UserName">
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Username</div>
</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap" tabindex="-1">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" tabindex="0" aria-label="Password Hash" name="name" type="name" value="@Model.PasswordHash" disabled="disabled">
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Username</div>
</div>
</div>
</div>
</div>
</label>
<div>
<button class="q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-primary text-white q-btn--actionable q-focusable q-hoverable">
<span class="q-focus-helper"></span>
<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
<span class="block">Save</span>
</span>
</button>
<button class="q-btn q-btn-item non-selectable no-outline q-btn--flat q-btn--rectangle text-primary q-btn--actionable q-focusable q-hoverable q-ml-sm" type="reset">
<span class="q-focus-helper"></span>
<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
<span class="block">Reset</span>
</span>
</button>
</div>
</form>
</div>
@await Html.PartialAsync("_FooterMenu")

View File

@ -2,5 +2,4 @@
@using (Html.BeginForm()){ @using (Html.BeginForm()){
@Html.LabelFor(x => x.Password, "Password") @Html.LabelFor(x => x.Password, "Password")
@Html.PasswordFor(x => x.Password); @Html.PasswordFor(x => x.Password);
@Html.NameFor(x => x.Email);
} }

View File

@ -0,0 +1,68 @@
<div class="q-pa-md q-gutter-sm">
<div class="row">
<div class="col col-md-8">
<div class="q-breadcrumbs">
<div class="flex items-center justify-start q-gutter-sm">
<div class="flex items-center text-primary">
<a class="q-breadcrumbs__el q-link flex inline items-center relative-position q-link--focusable">
<i class="q-icon notranslate material-icons q-breadcrumbs__el-icon q-breadcrumbs__el-icon--with-label">share_location</i>
System
</a>
</div>
<div class="q-breadcrumbs__separator">/</div>
<div class="flex items-center q-breadcrumbs--last">
<span class="q-breadcrumbs__el q-link flex inline items-center relative-position q-link--focusable">Login</span>
</div>
</div>
</div>
</div>
</div>
<div class="new-user-container">
<div class="q-pa-md" style="max-width: 400px;">
<form method="POST" action="LoginAction" class="q-form q-gutter-md">
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" name="email" aria-label="Email" type="email">
</div>
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Email address</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap" tabindex="-1">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" tabindex="0" aria-label="Password" name="password" type="password">
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Password</div>
</div>
</div>
</div>
</div>
</label>
<div>
<button class="q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-primary text-white q-btn--actionable q-focusable q-hoverable">
<span class="q-focus-helper"></span>
<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
<span class="block">Login</span>
</span>
</button>
<button class="q-btn q-btn-item non-selectable no-outline q-btn--flat q-btn--rectangle text-primary q-btn--actionable q-focusable q-hoverable q-ml-sm" type="reset">
<span class="q-focus-helper"></span>
<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
<span class="block">Reset</span>
</span>
</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,117 @@
<div class="q-pa-md q-gutter-sm">
<div class="row">
<div class="col col-md-8">
<div class="q-breadcrumbs">
<div class="flex items-center justify-start q-gutter-sm">
<div class="flex items-center text-primary">
<a class="q-breadcrumbs__el q-link flex inline items-center relative-position q-link--focusable">
<i class="q-icon notranslate material-icons q-breadcrumbs__el-icon q-breadcrumbs__el-icon--with-label">share_location</i>
System
</a>
</div>
<div class="q-breadcrumbs__separator">/</div>
<div class="flex items-center q-breadcrumbs--last">
<span class="q-breadcrumbs__el q-link flex inline items-center relative-position q-link--focusable">Login</span>
</div>
</div>
</div>
</div>
</div>
<div class="new-user-container">
<div class="q-pa-md" style="max-width: 400px;">
<form method="POST" action="SignupAction" class="q-form q-gutter-md">
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" name="email" aria-label="Email" type="email">
<div class="q-field__label no-pointer-events absolute ellipsis">Name</div>
</div>
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Email address</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" name="username" aria-label="username" type="name">
<div class="q-field__label no-pointer-events absolute ellipsis">Username</div>
</div>
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Username</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" name="phoneNumber" aria-label="phoneNumber" type="phoneNumber">
<div class="q-field__label no-pointer-events absolute ellipsis">Phone Number</div>
</div>
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Phone Number</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap" tabindex="-1">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" tabindex="0" aria-label="Password" name="password" type="password">
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Password</div>
</div>
</div>
</div>
</div>
</label>
<label class="q-field row no-wrap items-start q-field--filled q-input q-field--labeled q-field--dark q-field--with-bottom">
<div class="q-field__inner relative-position col self-stretch">
<div class="q-field__control relative-position row no-wrap" tabindex="-1">
<div class="q-field__control-container col relative-position row no-wrap q-anchor--skip">
<input class="q-field__native q-placeholder" tabindex="0" aria-label="confirmPassword" name="confirmPassword" type="password">
</div>
<div class="q-field__bottom row items-start q-field__bottom--animated">
<div class="q-field__messages col">
<div>Confirm Password</div>
</div>
</div>
</div>
</div>
</label>
<div>
<button class="q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-primary text-white q-btn--actionable q-focusable q-hoverable">
<span class="q-focus-helper"></span>
<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
<span class="block">Login</span>
</span>
</button>
<button class="q-btn q-btn-item non-selectable no-outline q-btn--flat q-btn--rectangle text-primary q-btn--actionable q-focusable q-hoverable q-ml-sm" type="reset">
<span class="q-focus-helper"></span>
<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row">
<span class="block">Reset</span>
</span>
</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,6 @@
<div class="text-center">
<h1 class="display-4">Banks</h1>
</div>
@await Html.PartialAsync("_FooterMenu");

View File

@ -1,11 +1,6 @@
@using BlueWest.Views.Utils
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center"> <div class="text-center">
<h1 class="display-4">Companies Module</h1> <h1 class="display-4">Companies Module</h1>
</div> </div>
@Html.Partial("_FooterMenu"); @await Html.PartialAsync("_FooterMenu");

View File

@ -1,11 +1,6 @@
@using BlueWest.Views.Utils
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center"> <div class="text-center">
<h1 class="display-4">Data Module</h1> <h1 class="display-4">Data Module</h1>
</div> </div>
@Html.Partial("_FooterMenu"); @await Html.PartialAsync("_FooterMenu");

View File

@ -0,0 +1,11 @@
@using BlueWest.Views.Utils
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Currencies Module</h1>
</div>
@await Html.PartialAsync("_FooterMenu");

View File

@ -1,11 +1,7 @@
@using BlueWest.Views.Utils
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center"> <div class="text-center">
<h1 class="display-4">Data Module</h1> <h1 class="display-4">Data Module</h1>
</div> </div>
@Html.Partial("_FooterMenu"); @await Html.PartialAsync("_FooterMenu");

View File

@ -1,14 +1,5 @@
@using Microsoft.AspNetCore.Localization <div class="text-center">
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1> <h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
<table class="table culture-table"> <table class="table culture-table">
<tr> <tr>
<td>Date</td> <td>Date</td>

View File

@ -0,0 +1,11 @@
@using BlueWest.Views.Utils
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Login Module</h1>
</div>
@Html.Partial("_FooterMenu");

View File

@ -1,10 +0,0 @@
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Jobs Module</h1>
</div>
@Html.Partial("_FooterMenu");

View File

@ -1,59 +0,0 @@
<footer class="q-footer q-layout__section--marginal absolute-bottom q-footer--bordered text-secondary">
<div class="q-tabs row no-wrap items-center q-tabs--not-scrollable q-tabs--horizontal q-tabs__arrows--inside text-white">
<div class="q-tabs__content row no-wrap items-center self-stretch hide-scrollbar relative-position q-tabs__content--align-center">
<div style="margin-right: 1rem;">
<a href="/geo" class="router-link-active router-link-exact-active">
<i class="q-icon text-white notranslate material-icons" style="font-size: 24px;">data_usage</i>
</a>
</div>
<a href="/system/roles" class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--no-caps q-focusable q-hoverable cursor-pointer">
<div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
<div class="q-tab__label">Roles</div>
</div>
<div class="q-tab__indicator absolute-bottom text-transparent">
</div>
</a>
<a href="/data/countries"
class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--no-caps q-focusable q-hoverable cursor-pointer">
<div class="q-focus-helper"></div>
<div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
<div class="q-tab__label">Sessions</div>
</div>
<div class="q-tab__indicator absolute-bottom text-transparent"></div>
</a><!--<FooterMenuItem>-->
<a href="/data/currencies"
class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--no-caps q-focusable q-hoverable cursor-pointer">
<div class="q-focus-helper"></div>
<div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
<div class="q-tab__label">Settings</div>
</div>
<div class="q-tab__indicator absolute-bottom text-transparent"></div>
</a><!--<FooterMenuItem>-->
<a href="/data/banks"
class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--no-caps q-focusable q-hoverable cursor-pointer">
<div class="q-focus-helper"></div>
<div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
<div class="q-tab__label">Banks</div>
</div>
<div class="q-tab__indicator absolute-bottom text-transparent"></div>
</a><!--<FooterMenuItem>-->
<!--<FooterMenuItem>-->
<button class="q-btn q-btn-item non-selectable no-outline q-btn--flat q-btn--round q-btn--actionable q-focusable q-hoverable q-btn--dense s-7IPF32Wcq3s8"
type="button">
<span class="q-focus-helper s-7IPF32Wcq3s8"></span>
<span
class="q-btn__content text-center col items-center q-anchor--skip justify-center row s-7IPF32Wcq3s8">
<i
class="q-icon notranslate material-icons s-7IPF32Wcq3s8">
menu
</i>
</span>
</button>
</div>
</div>
</footer>

View File

@ -4,28 +4,33 @@
var menu = LayoutCache.GetDefaultFooterMenu(ViewData); var menu = LayoutCache.GetDefaultFooterMenu(ViewData);
} }
<footer class="q-footer q-layout__section--marginal absolute-bottom q-footer--bordered text-secondary">
<div class="q-tabs row no-wrap items-center q-tabs--not-scrollable q-tabs--horizontal q-tabs__arrows--inside text-white">
<div class="q-tabs__content row no-wrap items-center self-stretch hide-scrollbar relative-position q-tabs__content--align-center">
<div style="margin-right: 1rem;"> @if (menu.Count > 0)
<a href="/geo" class="router-link-active router-link-exact-active"> {
<i class="q-icon text-white notranslate material-icons" style="font-size: 24px;">data_usage</i> <footer class="q-footer q-layout__section--marginal absolute-bottom q-footer--bordered text-secondary">
</a> <div class="q-tabs row no-wrap items-center q-tabs--not-scrollable q-tabs--horizontal q-tabs__arrows--inside text-white">
</div> <div class="q-tabs__content row no-wrap items-center self-stretch hide-scrollbar relative-position q-tabs__content--align-center">
@if (menu is {Count: > 0 })
{ <div style="margin-right: 1rem;">
@foreach (var record in menu) <a href="/geo" class="router-link-active router-link-exact-active">
<i class="q-icon text-white notranslate material-icons" style="font-size: 24px;">data_usage</i>
</a>
</div>
@if (menu is {Count: > 0 })
{ {
<a href="@record.Location" class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--no-caps q-focusable q-hoverable cursor-pointer"> @foreach (var record in menu)
<div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column"> {
<div class="q-tab__label">@record.Name</div> <a href="@record.Location" class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--no-caps q-focusable q-hoverable cursor-pointer">
</div> <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
<div class="q-tab__indicator absolute-bottom text-transparent"></div> <div class="q-tab__label">@record.Name</div>
</a> </div>
<div class="q-tab__indicator absolute-bottom text-transparent"></div>
</a>
}
} }
}
</div>
</div> </div>
</div> </footer>
</footer> }

View File

@ -1,7 +1,13 @@
@using BlueWest.Views.Utils @using BlueWest.Views.Utils
@using Duende.IdentityServer.Extensions
@using BlueWest.Data.Auth
@{ @{
Layout = null; Layout = null;
var menu = LayoutCache.GetDefaultHeaderMenu(ViewData); var userAuthenticated = User.IsAuthenticated();
var menu = LayoutCache.GetDefaultHeaderMenu(ViewData, userAuthenticated);
var user = ViewData.GetUserViewData();
var rootUrl = SessionConstants.CookieDomain;
} }
<div class="q-tabs items-center row no-wrap items-center q-tabs--not-scrollable q-tabs--horizontal q-tabs__arrows--inside"> <div class="q-tabs items-center row no-wrap items-center q-tabs--not-scrollable q-tabs--horizontal q-tabs__arrows--inside">
@ -16,8 +22,21 @@
</div> </div>
<div class="q-tab__indicator absolute-bottom"></div> <div class="q-tab__indicator absolute-bottom"></div>
</a> </a>
} }
}
@if (userAuthenticated && user != null)
{
<div class="q-chip row inline no-wrap items-center q-chip--dark q-dark">
<div class="q-chip__content col row no-wrap items-center q-anchor--skip">
<div class="q-avatar">
<div class="q-avatar__content row flex-center overflow-hidden">
<img src="@rootUrl/static/profile/boy-avatar.png">
</div>
</div> @user.Email
</div>
</div>
} }
</div> </div>
</div> </div>

View File

@ -1,33 +1,36 @@
<!DOCTYPE html> @using BlueWest.Views.Utils
@using BlueWest.Data.Auth
@{
var applicationUser = ViewData.GetUserViewData();
var rootUrl = SessionConstants.CookieDomain;
}
<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>@ViewData["Title"] - BlueWest.Views</title> <title>@ViewData["Title"] - BlueWest.Views</title>
<link rel="stylesheet" href="~/static/main.css"/> <link rel="stylesheet" href="@rootUrl/static/main.css"/>
@* @*
<link rel="stylesheet" href="~/BlueWest.Views.styles.css" asp-append-version="true"/> <link rel="stylesheet" href="~/BlueWest.Views.styles.css" asp-append-version="true"/>
*@ *@
<link rel="preload" href="/static/material-icons/web-font/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.woff" as="font" type="font/woff" crossorigin> <link rel="preload" href="@rootUrl/static/material-icons/web-font/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.woff" as="font" type="font/woff" crossorigin>
<link rel="preload" href="/static/material-icons/web-font/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2" as="font" type="font/woff2" crossorigin> <link rel="preload" href="@rootUrl/static/material-icons/web-font/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2" as="font" type="font/woff2" crossorigin>
<style> <style>
@@font-face { @@font-face {
font-family: 'Material Icons'; font-family: 'Material Icons';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url("static/material-icons/web-font/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2") format('woff2'), url('static/material-icons/web-font/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.woff') format('woff'); src: url("@rootUrl/static/material-icons/web-font/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2") format('woff2'), url('static/material-icons/web-font/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.woff') format('woff');
font-display: swap; font-display: swap;
} }
</style> </style>
</head> </head>
<body class="desktop touch body--dark" style="--q-primary:#414141;"> <body class="desktop touch body--dark" style="--q-primary:#414141;">
<div id="q-app"> <div id="q-app">
@RenderBody()
<div class="q-layout q-layout--standard" id="app-entry"> <div class="q-layout q-layout--standard" id="app-entry">
<header class="q-header q-layout__section--marginal fixed-top bg-primary text-white"> <header class="q-header q-layout__section--marginal fixed-top bg-primary text-white">
@await Html.PartialAsync("_HeaderMenu") @await Html.PartialAsync("_HeaderMenu", applicationUser)
</header> </header>
<div class="q-page-container" style="padding-top: 48px; padding-bottom: 49px;"> <div class="q-page-container" style="padding-top: 48px; padding-bottom: 49px;">

View File

@ -1,8 +1,3 @@
@using BlueWest.Views.Utils
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center"> <div class="text-center">
<h1 class="display-4">System Module</h1> <h1 class="display-4">System Module</h1>
</div> </div>

View File

@ -0,0 +1,5 @@
<div class="text-center">
<h1 class="display-4">Users</h1>
</div>
@await Html.PartialAsync("_FooterMenu");

View File

@ -0,0 +1,6 @@
<div class="text-center">
<h1 class="display-4">Roles Module</h1>
</div>
@await Html.PartialAsync("_FooterMenu");

View File

@ -1,10 +1,7 @@
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center"> <div class="text-center">
<h1 class="display-4">Users</h1> <h1 class="display-4">Users</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div> </div>
@Html.Partial("~/Views/Shared/UsersMenu.cshtml");
@await Html.PartialAsync("_FooterMenu");

View File

@ -5,14 +5,15 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"mode": "no-docker",
"AllowedHosts": "*", "AllowedHosts": "*",
"ConnectionStringDocker": { "ConnectionStringDocker": {
"MySQL": "server=db;user=blueuser;password=dXjw127124dJ;database=bluedb;", "Redis": "redis://redisinstance:6379",
"Redis": "redis://redisinstance:6379" "MySQL": "server=db;user=blueuser;password=dXjw127124dJ;database=bluedb;"
}, },
"ConnectionStringNoDocker": { "ConnectionStringNoDocker": {
"MySQL": "server=localhost;user=blueuser;password=dXjw127124dJ;database=bluedb;", "Redis": "redis://localhost:6379",
"Redis": "redis://localhost:6379" "MySQL": "server=localhost;user=blueuser;password=dXjw127124dJ;database=bluedb;"
}, },
"AuthSettings": { "AuthSettings": {
"SecretKey": "iJWHDmHLpUA283sqsfhqGbMRdRj1PVkH" "SecretKey": "iJWHDmHLpUA283sqsfhqGbMRdRj1PVkH"

View File

@ -1,4 +0,0 @@
{
"mode": "no-docker",
"database": "mysql"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

View File

@ -41,8 +41,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueWest.Views", "BlueWest.
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueWest.Domain", "BlueWest.Domain\BlueWest.Domain.csproj", "{1085FF6E-E568-441E-9A2D-23F50AB613AF}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueWest.Domain", "BlueWest.Domain\BlueWest.Domain.csproj", "{1085FF6E-E568-441E-9A2D-23F50AB613AF}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueWest.Razor.Library", "BlueWest.Razor.Library\BlueWest.Razor.Library.csproj", "{CA6DF60F-B33E-4688-A4ED-4427B446E852}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueWest.Data.Auth", "BlueWest.Data.Auth\BlueWest.Data.Auth.csproj", "{2998FE17-18AD-4888-A696-7F6340F8A543}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlueWest.Data.Auth", "BlueWest.Data.Auth\BlueWest.Data.Auth.csproj", "{2998FE17-18AD-4888-A696-7F6340F8A543}"
EndProject EndProject
Global Global
@ -95,10 +93,6 @@ Global
{1085FF6E-E568-441E-9A2D-23F50AB613AF}.Debug|Any CPU.Build.0 = Debug|Any CPU {1085FF6E-E568-441E-9A2D-23F50AB613AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1085FF6E-E568-441E-9A2D-23F50AB613AF}.Release|Any CPU.ActiveCfg = Release|Any CPU {1085FF6E-E568-441E-9A2D-23F50AB613AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1085FF6E-E568-441E-9A2D-23F50AB613AF}.Release|Any CPU.Build.0 = Release|Any CPU {1085FF6E-E568-441E-9A2D-23F50AB613AF}.Release|Any CPU.Build.0 = Release|Any CPU
{CA6DF60F-B33E-4688-A4ED-4427B446E852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CA6DF60F-B33E-4688-A4ED-4427B446E852}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CA6DF60F-B33E-4688-A4ED-4427B446E852}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CA6DF60F-B33E-4688-A4ED-4427B446E852}.Release|Any CPU.Build.0 = Release|Any CPU
{2998FE17-18AD-4888-A696-7F6340F8A543}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2998FE17-18AD-4888-A696-7F6340F8A543}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2998FE17-18AD-4888-A696-7F6340F8A543}.Debug|Any CPU.Build.0 = Debug|Any CPU {2998FE17-18AD-4888-A696-7F6340F8A543}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2998FE17-18AD-4888-A696-7F6340F8A543}.Release|Any CPU.ActiveCfg = Release|Any CPU {2998FE17-18AD-4888-A696-7F6340F8A543}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@ -4,8 +4,8 @@ services:
container_name: BW1_MYSQL container_name: BW1_MYSQL
image: mysql/mysql-server:8.0 image: mysql/mysql-server:8.0
environment: environment:
MYSQL_ROOT_HOST: localhost MYSQL_ROOT_HOST: '172.19.0.1'
MYSQL_USER_HOST: localhost MYSQL_USER_HOST: '172.19.0.1'
MYSQL_ROOT_PASSWORD: dXjw127124dJ MYSQL_ROOT_PASSWORD: dXjw127124dJ
MYSQL_USER: blueuser MYSQL_USER: blueuser
MYSQL_PASSWORD: dXjw127124dJ MYSQL_PASSWORD: dXjw127124dJ
@ -14,14 +14,6 @@ services:
- "3306:3306" - "3306:3306"
volumes: volumes:
- ./docker-entrypoint-initdb.d/:/docker-entrypoint-initdb.d/ - ./docker-entrypoint-initdb.d/:/docker-entrypoint-initdb.d/
phpmyadmin:
container_name: BW_PHPMYADMIN
image: phpmyadmin/phpmyadmin
ports:
- 80:80
environment:
MYSQL_USERNAME: 'blueuser'
MYSQL_ROOT_PASSWORD: 'dXjw127124dJ'
redis-instance: redis-instance:
image: "redislabs/redismod" image: "redislabs/redismod"
ports: ports: