Add authentication and authorization #21

Merged
Egamorf merged 9 commits from feature/authentification-and-authorization into main 2025-04-21 01:53:59 +02:00
55 changed files with 2186 additions and 317 deletions

View File

@@ -37,6 +37,9 @@ Store your favorite games, intelligent game add, store game files and data, mana
| DB_USERNAME | Username for the database | | DB_USERNAME | Username for the database |
| DB_PASSWORD | Plain password for the database | | DB_PASSWORD | Plain password for the database |
| DB_DATABASE | Name of the database | | DB_DATABASE | Name of the database |
| JWT_KEY | Key for your jwt tokens |
| JWT_ISSUER | Your domain name |
| JWT_AUDIENCE | Your domain name |
<!-- ## Installation <!-- ## Installation

View File

@@ -1,14 +1,18 @@
@using GameIdeas.BlazorApp.Layouts @using GameIdeas.BlazorApp.Layouts
@using Microsoft.AspNetCore.Components.Authorization
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -9,10 +9,13 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Blazored.FluentValidation" Version="2.2.0" /> <PackageReference Include="Blazored.FluentValidation" Version="2.2.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="FluentValidation" Version="11.11.0" /> <PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.2" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.2" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,12 +1,26 @@
using GameIdeas.Shared.Dto; using GameIdeas.Shared.Dto;
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
namespace GameIdeas.BlazorApp.Helpers; namespace GameIdeas.BlazorApp.Helpers;
public static class GameHelper public static class GameHelper
{ {
public static void WriteTrackingDto(GameDetailDto game) public static void WriteTrackingDto(GameDetailDto game, AuthenticationState authState)
{ {
game.CreationUserId = 100000; if (authState == null)
{
throw new ArgumentNullException(nameof(authState), "Authentication state missing");
}
var userId = authState.User.FindFirstValue(ClaimTypes.Sid);
if (userId == null)
{
throw new ArgumentNullException(nameof(authState), "user state missing");
}
game.CreationUserId = userId;
game.CreationDate = DateTime.Now; game.CreationDate = DateTime.Now;
} }

View File

@@ -5,8 +5,10 @@ using GameIdeas.BlazorApp.Shared.Components.Select.Models;
using GameIdeas.BlazorApp.Shared.Components.Slider; using GameIdeas.BlazorApp.Shared.Components.Slider;
using GameIdeas.Shared.Dto; using GameIdeas.Shared.Dto;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Forms;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using System.Security.Claims;
namespace GameIdeas.BlazorApp.Pages.Games.Components; namespace GameIdeas.BlazorApp.Pages.Games.Components;
@@ -14,6 +16,7 @@ public partial class GameCreationForm
{ {
[Inject] private IJSRuntime Js { get; set; } = default!; [Inject] private IJSRuntime Js { get; set; } = default!;
[Inject] private IGameGateway GameGateway { get; set; } = default!; [Inject] private IGameGateway GameGateway { get; set; } = default!;
[Inject] private AuthenticationStateProvider AuthenticationState { get; set; } = default!;
[CascadingParameter] private Popup? Popup { get; set; } [CascadingParameter] private Popup? Popup { get; set; }
[Parameter] public CategoriesDto? Categories { get; set; } [Parameter] public CategoriesDto? Categories { get; set; }
[Parameter] public EventCallback OnSubmit { get; set; } [Parameter] public EventCallback OnSubmit { get; set; }
@@ -33,7 +36,6 @@ public partial class GameCreationForm
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
await Js.InvokeVoidAsync("resizeGameForm"); await Js.InvokeVoidAsync("resizeGameForm");
} }
private void HandleOnCancel() private void HandleOnCancel()
@@ -52,7 +54,9 @@ public partial class GameCreationForm
{ {
IsLoading = true; IsLoading = true;
GameHelper.WriteTrackingDto(GameDto); var authState = await AuthenticationState.GetAuthenticationStateAsync();
GameHelper.WriteTrackingDto(GameDto, authState);
var gameId = await GameGateway.CreateGame(GameDto); var gameId = await GameGateway.CreateGame(GameDto);
if (gameId != 0) if (gameId != 0)
@@ -68,6 +72,7 @@ public partial class GameCreationForm
finally finally
{ {
IsLoading = false; IsLoading = false;
StateHasChanged();
} }
} }
} }

View File

@@ -1,9 +1,11 @@
@using GameIdeas.BlazorApp.Pages.Games @using GameIdeas.BlazorApp.Pages.Games
@using GameIdeas.BlazorApp.Shared.Components.Account @using GameIdeas.BlazorApp.Pages.User
@using GameIdeas.BlazorApp.Shared.Components.Select @using GameIdeas.BlazorApp.Shared.Components.Select
@using GameIdeas.BlazorApp.Shared.Components.Select.Models @using GameIdeas.BlazorApp.Shared.Components.Select.Models
@using GameIdeas.BlazorApp.Shared.Models @using GameIdeas.BlazorApp.Shared.Models
@using GameIdeas.Resources @using GameIdeas.Resources
@using GameIdeas.Shared.Constants
@using Microsoft.AspNetCore.Components.Authorization
@inherits ComponentBase @inherits ComponentBase
@@ -15,30 +17,26 @@
@ChildContent @ChildContent
<div class="account-add-container"> <div class="account-add-container">
<div class="add-container"> <AuthorizeView Roles="@GlobalConstants.ADMIN_MEMBER">
<div class="add-buttons"> <Authorized>
<div class="first-button button"> <div class="add-buttons">
<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <div class="first-button button">
<path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
</svg>
</div>
<Select @ref="SelectListAdd" TItem="KeyValuePair<AddType, string>" THeader="object"
ValuesChanged=HandleAddTypeClicked Params=SelectParams Theme="SelectTheme.Navigation">
<div class="second-button button">
<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M1 3H23L12 22" /> <path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
</svg> </svg>
</div> </div>
</Select> <Select @ref="SelectListAdd" TItem="KeyValuePair<AddType, string>" THeader="object"
</div> ValuesChanged=HandleAddTypeClicked Params=SelectParams Theme="SelectTheme.Navigation">
</div> <div class="second-button button">
<div class="account-container"> <svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<div class="icon-container" @onclick=HandleAccountClicked> <path d="M1 3H23L12 22" />
<svg class="account-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> </svg>
<path d="M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z" /> </div>
</svg> </Select>
</div> </div>
<AccountSettings @ref="AccountSettings" /> </Authorized>
</div> </AuthorizeView>
<UserMenu />
</div> </div>
</div> </div>

View File

@@ -1,4 +1,3 @@
using GameIdeas.BlazorApp.Shared.Components.Account;
using GameIdeas.BlazorApp.Shared.Components.Select; using GameIdeas.BlazorApp.Shared.Components.Select;
using GameIdeas.BlazorApp.Shared.Components.Select.Models; using GameIdeas.BlazorApp.Shared.Components.Select.Models;
using GameIdeas.BlazorApp.Shared.Models; using GameIdeas.BlazorApp.Shared.Models;
@@ -18,7 +17,6 @@ public partial class GameHeader : ComponentBase
{ AddType.Auto, ResourcesKey.AutoAdd } { AddType.Auto, ResourcesKey.AutoAdd }
}; };
private AccountSettings? AccountSettings;
private Select<KeyValuePair<AddType, string>, object>? SelectListAdd; private Select<KeyValuePair<AddType, string>, object>? SelectListAdd;
private SelectParams<KeyValuePair<AddType, string>, object> SelectParams = new(); private SelectParams<KeyValuePair<AddType, string>, object> SelectParams = new();
@@ -26,7 +24,7 @@ public partial class GameHeader : ComponentBase
{ {
SelectParams = new() SelectParams = new()
{ {
Items = AddTypes.ToList(), Items = [.. AddTypes],
GetItemLabel = item => item.Value GetItemLabel = item => item.Value
}; };
@@ -43,9 +41,4 @@ public partial class GameHeader : ComponentBase
SelectListAdd?.Close(); SelectListAdd?.Close();
await AddTypeChanged.InvokeAsync(values.FirstOrDefault().Key); await AddTypeChanged.InvokeAsync(values.FirstOrDefault().Key);
} }
private void HandleAccountClicked()
{
AccountSettings?.Toggle();
}
} }

View File

@@ -14,6 +14,7 @@
align-items: center; align-items: center;
width: 40px; width: 40px;
height: 100%; height: 100%;
cursor: pointer;
} }
.icon-container img { .icon-container img {
@@ -21,10 +22,6 @@
max-width: 85%; max-width: 85%;
} }
.icon-container:hover {
cursor: pointer;
}
.account-add-container { .account-add-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -32,15 +29,12 @@
align-items: flex-end; align-items: flex-end;
} }
.add-container {
margin-right: 40px;
}
.add-buttons { .add-buttons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
background: var(--violet); background: var(--violet);
border-radius: var(--small-radius); border-radius: var(--small-radius);
margin-right: 40px;
} }
.button { .button {
@@ -70,8 +64,4 @@
.button-icon:hover { .button-icon:hover {
background: var(--violet-selected); background: var(--violet-selected);
cursor: pointer; cursor: pointer;
} }
.account-icon {
fill: var(--line);
}

View File

@@ -0,0 +1,29 @@
@using Blazored.FluentValidation
<EditForm EditContext="EditContext" OnSubmit="HandleLoginSubmit">
<FluentValidationValidator />
<div class="login-form">
<div class="login-field">
<div class="input-title">@ResourcesKey.EnterUsername</div>
<InputText class="input-text"
@bind-Value="UserDto.Username" />
</div>
<div class="login-field">
<div class="input-title">@ResourcesKey.EnterPassword</div>
<InputText class="input-text"
@bind-Value="UserDto.Password" />
</div>
<div class="login-field">
<button class="login-button" type="submit" disabled="@IsLoading">
@if (IsLoading)
{
<div class="loading"></div>
}
else
{
@ResourcesKey.Login
}
</button>
</div>
</div>
</EditForm>

View File

@@ -0,0 +1,44 @@
using GameIdeas.BlazorApp.Pages.User.Gateways;
using GameIdeas.Shared.Dto;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
namespace GameIdeas.BlazorApp.Pages.User.Components;
public partial class Login
{
[Parameter] public IAuthGateway AuthGateway { get; set; } = default!;
private EditContext? EditContext;
private UserDto UserDto = new();
private bool IsLoading = false;
protected override void OnInitialized()
{
EditContext = new EditContext(UserDto);
}
private async Task HandleLoginSubmit()
{
if (EditContext?.Validate() == false)
{
return;
}
try
{
IsLoading = true;
await AuthGateway.Login(UserDto);
}
catch (Exception)
{
UserDto.Password = string.Empty;
EditContext?.Validate();
}
finally
{
IsLoading = false;
StateHasChanged();
}
}
}

View File

@@ -1,21 +1,4 @@
.account-setting-content { .login-form {
overflow: hidden;
border-radius: var(--big-radius);
position: fixed;
animation-name: fade-in;
animation-duration: 0.4s;
border: 2px solid var(--input-selected);
background: var(--dropdown-content);
right: 10px;
margin-top: 4px;
z-index: var(--index-floating);
}
.invisible {
display: none;
}
.login-form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 20px 8px; padding: 20px 8px;
@@ -53,7 +36,7 @@
.login-button:hover { .login-button:hover {
background: var(--violet-selected); background: var(--violet-selected);
cursor:pointer; cursor: pointer;
} }
.login-button:disabled { .login-button:disabled {
@@ -70,26 +53,3 @@
animation: loading 1s linear infinite; animation: loading 1s linear infinite;
justify-self: center; justify-self: center;
} }
.settings-list {
display: flex;
flex-direction: column;
}
.line {
margin: 0 6px;
border-bottom: 2px solid var(--line);
}
.settings-element {
max-width: 140px;
height: 40px;
padding: 0 26px;
align-content: center;
}
.settings-element:hover {
background: var(--line)
}

View File

@@ -1,9 +1,9 @@
using FluentValidation; using FluentValidation;
using GameIdeas.Shared.Dto; using GameIdeas.Shared.Dto;
namespace GameIdeas.BlazorApp.Shared.Components.Account; namespace GameIdeas.BlazorApp.Pages.User.Components;
public class LoginValidator : AbstractValidator<LoginDto> public class LoginValidator : AbstractValidator<UserDto>
{ {
public LoginValidator() public LoginValidator()
{ {

View File

@@ -0,0 +1,38 @@
using GameIdeas.BlazorApp.Services;
using GameIdeas.BlazorApp.Shared.Constants;
using GameIdeas.BlazorApp.Shared.Exceptions;
using GameIdeas.Resources;
using GameIdeas.Shared.Dto;
using Microsoft.AspNetCore.Components.Authorization;
namespace GameIdeas.BlazorApp.Pages.User.Gateways;
public class AuthGateway(IHttpClientService httpClient,
AuthenticationStateProvider stateProvider) : IAuthGateway
{
public async Task<bool> Login(UserDto userDto)
{
try
{
var token = await httpClient.PostAsync<TokenDto>(Endpoints.Auth.Login, userDto);
await ((JwtAuthenticationStateProvider)stateProvider).NotifyUserAuthenticationAsync(token!.Token!);
return true;
}
catch (Exception)
{
throw new AuthenticationUserException(ResourcesKey.UserLoginFailed);
}
}
public async Task Logout()
{
try
{
await ((JwtAuthenticationStateProvider)stateProvider).NotifyUserLogoutAsync();
}
catch (Exception)
{
throw new AuthenticationUserException(ResourcesKey.UserLogoutFailed);
}
}
}

View File

@@ -0,0 +1,9 @@
using GameIdeas.Shared.Dto;
namespace GameIdeas.BlazorApp.Pages.User.Gateways;
public interface IAuthGateway
{
Task<bool> Login(UserDto userDto);
Task Logout();
}

View File

@@ -0,0 +1,51 @@
@using GameIdeas.BlazorApp.Pages.User.Components
@using GameIdeas.BlazorApp.Shared.Components.BackdropFilter
@using GameIdeas.BlazorApp.Shared.Constants
@using GameIdeas.Shared.Constants
@using Microsoft.AspNetCore.Components.Authorization
<div class="menu">
<div class="icon" @onclick=HandleAccountClicked>
@Icons.Account
</div>
<div class="container">
@if (ContentVisile)
{
<div class="content">
<AuthorizeView Roles="@GlobalConstants.ADMIN_MEMBER">
<Authorized>
<div class="menu-element">
@ResourcesKey.CategoriesManager
</div>
<span class="line"></span>
</Authorized>
</AuthorizeView>
<AuthorizeView Roles="@GlobalConstants.ADMINISTRATOR">
<Authorized>
<div class="menu-element">
@ResourcesKey.UserManager
</div>
<span class="line"></span>
</Authorized>
</AuthorizeView>
<AuthorizeView>
<Authorized>
<div class="menu-element" @onclick="HandleLogoutClicked">
@ResourcesKey.Logout
</div>
</Authorized>
<NotAuthorized>
<Login AuthGateway="AuthGateway" />
</NotAuthorized>
</AuthorizeView>
</div>
}
</div>
</div>
<BackdropFilter AllowBodyScroll=true CloseOnClick=true Color="BackdropFilterColor.Transparent"
IsVisible="ContentVisile" OnClick="HandleBackdropFilterClicked" />

View File

@@ -0,0 +1,29 @@
using GameIdeas.BlazorApp.Pages.User.Gateways;
using GameIdeas.BlazorApp.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
namespace GameIdeas.BlazorApp.Pages.User;
public partial class UserMenu
{
[Inject] private IAuthGateway AuthGateway { get; set; } = default!;
private bool ContentVisile = false;
private async Task HandleLogoutClicked()
{
await AuthGateway.Logout();
ContentVisile = false;
}
private void HandleAccountClicked()
{
ContentVisile = true;
}
private void HandleBackdropFilterClicked()
{
ContentVisile = false;
}
}

View File

@@ -0,0 +1,48 @@
.menu {
position: relative;
}
.icon {
display: flex;
justify-content: center;
align-items: center;
width: 40px;
cursor: pointer;
}
.icon ::deep svg {
fill: var(--line);
}
.container {
right: 0;
position: absolute;
margin-top: 4px;
z-index: var(--index-dropdown);
}
.content {
overflow: hidden;
border-radius: var(--big-radius);
border: 2px solid var(--input-selected);
background: var(--dropdown-content);
display: flex;
flex-direction: column;
}
.line {
margin: 0 6px;
border-bottom: 2px solid var(--input-selected);
}
.menu-element {
height: 32px;
padding: 0 20px;
align-content: center;
text-wrap: nowrap;
cursor: pointer;
}
.menu-element:hover {
background: var(--input-selected)
}

View File

@@ -1,12 +1,17 @@
using System.Net.Http.Json; using System.Net.Http.Json;
using Blazored.LocalStorage;
using GameIdeas.BlazorApp; using GameIdeas.BlazorApp;
using GameIdeas.BlazorApp.Pages.Games.Gateways; using GameIdeas.BlazorApp.Pages.Games.Gateways;
using GameIdeas.BlazorApp.Pages.User.Gateways;
using GameIdeas.BlazorApp.Services; using GameIdeas.BlazorApp.Services;
using GameIdeas.Resources; using GameIdeas.Resources;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
var services = builder.Services;
builder.RootComponents.Add<App>("#app"); builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after"); builder.RootComponents.Add<HeadOutlet>("head::after");
@@ -15,7 +20,7 @@ UriBuilder uriBuilder = new(builder.HostEnvironment.BaseAddress)
Port = 8000 Port = 8000
}; };
builder.Services.AddHttpClient( services.AddHttpClient(
"GameIdeas.WebAPI", "GameIdeas.WebAPI",
client => client =>
{ {
@@ -23,11 +28,18 @@ builder.Services.AddHttpClient(
client.Timeout = TimeSpan.FromMinutes(3); client.Timeout = TimeSpan.FromMinutes(3);
}); });
builder.Services.AddScoped<IHttpClientService, HttpClientService>(); services.AddBlazoredLocalStorage();
builder.Services.AddScoped<IGameGateway, GameGateway>(); services.AddAuthorizationCore();
builder.Services.AddSingleton<TranslationService>(); services.AddScoped<AuthenticationStateProvider, JwtAuthenticationStateProvider>();
builder.Services.AddSingleton<Translations>();
services.AddScoped<IHttpClientService, HttpClientService>();
services.AddScoped<IAuthGateway, AuthGateway>();
services.AddScoped<IGameGateway, GameGateway>();
services.AddSingleton<TranslationService>();
services.AddSingleton<Translations>();
var app = builder.Build(); var app = builder.Build();

View File

@@ -3,10 +3,15 @@ using System.Net.Http.Headers;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.Json; using System.Text.Json;
using System.Text; using System.Text;
using Blazored.LocalStorage;
using GameIdeas.Shared.Constants;
namespace GameIdeas.BlazorApp.Services; namespace GameIdeas.BlazorApp.Services;
public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory) : IHttpClientService public class HttpClientService(
IHttpClientFactory httpClientFactory,
ILoggerFactory loggerFactory,
ILocalStorageService localStorage) : IHttpClientService
{ {
private readonly HttpClient httpClient = httpClientFactory.CreateClient("GameIdeas.WebAPI"); private readonly HttpClient httpClient = httpClientFactory.CreateClient("GameIdeas.WebAPI");
private readonly ILogger<HttpClientService> logger = loggerFactory.CreateLogger<HttpClientService>(); private readonly ILogger<HttpClientService> logger = loggerFactory.CreateLogger<HttpClientService>();
@@ -25,6 +30,8 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact
public async Task<T?> PostAsync<T>(string url, object data) public async Task<T?> PostAsync<T>(string url, object data)
{ {
await SetAuthorizationHeader();
var jsonContent = JsonSerializer.Serialize(data, _optionsCamelCase); var jsonContent = JsonSerializer.Serialize(data, _optionsCamelCase);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(url, content); var response = await httpClient.PostAsync(url, content);
@@ -32,8 +39,11 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact
return await GetResultValue<T>(response, ResourcesKey.ErrorWhenPostingData); return await GetResultValue<T>(response, ResourcesKey.ErrorWhenPostingData);
} }
public async Task<T?> PutAsync<T>(string url, object data) public async Task<T?> PutAsync<T>(string url, object data)
{ {
await SetAuthorizationHeader();
var jsonContent = JsonSerializer.Serialize(data, _optionsCamelCase); var jsonContent = JsonSerializer.Serialize(data, _optionsCamelCase);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await httpClient.PutAsync(url, content); var response = await httpClient.PutAsync(url, content);
@@ -43,6 +53,7 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact
public async Task<T?> DeleteAsync<T>(string? url) public async Task<T?> DeleteAsync<T>(string? url)
{ {
await SetAuthorizationHeader();
var response = await httpClient.DeleteAsync(url); var response = await httpClient.DeleteAsync(url);
return await GetResultValue<T>(response, ResourcesKey.ErrorWhenDeletingData); return await GetResultValue<T>(response, ResourcesKey.ErrorWhenDeletingData);
@@ -50,6 +61,7 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact
public async Task<T?> FetchDataAsync<T>(string? url) public async Task<T?> FetchDataAsync<T>(string? url)
{ {
await SetAuthorizationHeader();
var response = await httpClient.GetAsync(url); var response = await httpClient.GetAsync(url);
return await GetResultValue<T>(response, ResourcesKey.ErrorWhenFetchingData); return await GetResultValue<T>(response, ResourcesKey.ErrorWhenFetchingData);
@@ -57,6 +69,7 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact
public async Task<byte[]?> FetchBytesAsync(string? url) public async Task<byte[]?> FetchBytesAsync(string? url)
{ {
await SetAuthorizationHeader();
var response = await httpClient.GetAsync(url); var response = await httpClient.GetAsync(url);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
@@ -71,6 +84,7 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact
public async Task<Stream?> FetchStreamAsync(string? url) public async Task<Stream?> FetchStreamAsync(string? url)
{ {
await SetAuthorizationHeader();
var response = await httpClient.GetAsync(url); var response = await httpClient.GetAsync(url);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
@@ -84,6 +98,8 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact
public async Task<T?> PostFileAsync<T>(string? url, Stream fileStream, string fileName, string contentType) public async Task<T?> PostFileAsync<T>(string? url, Stream fileStream, string fileName, string contentType)
{ {
await SetAuthorizationHeader();
using var content = new MultipartFormDataContent(); using var content = new MultipartFormDataContent();
var streamContent = new StreamContent(fileStream); var streamContent = new StreamContent(fileStream);
@@ -122,4 +138,11 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact
throw new HttpRequestException( throw new HttpRequestException(
$"{errorMessage} + StatusCode: {response.StatusCode} + Reason: {response.ReasonPhrase}"); $"{errorMessage} + StatusCode: {response.StatusCode} + Reason: {response.ReasonPhrase}");
} }
private async Task SetAuthorizationHeader()
{
var token = await localStorage.GetItemAsStringAsync(GlobalConstants.LS_AUTH_STORAGE_KEY);
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("bearer", token);
}
} }

View File

@@ -0,0 +1,48 @@
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using GameIdeas.Shared.Constants;
namespace GameIdeas.BlazorApp.Services;
public class JwtAuthenticationStateProvider(ILocalStorageService localStorage) : AuthenticationStateProvider
{
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var savedToken = await localStorage.GetItemAsStringAsync(GlobalConstants.LS_AUTH_STORAGE_KEY);
if (!string.IsNullOrWhiteSpace(savedToken))
{
try
{
var token = new JwtSecurityTokenHandler().ReadJwtToken(savedToken);
return new AuthenticationState(
new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "jwt"))
);
}
catch
{
await localStorage.RemoveItemAsync(GlobalConstants.LS_AUTH_STORAGE_KEY);
}
}
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
public async Task NotifyUserAuthenticationAsync(string token)
{
await localStorage.SetItemAsStringAsync(GlobalConstants.LS_AUTH_STORAGE_KEY, token);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task NotifyUserLogoutAsync()
{
await localStorage.RemoveItemAsync(GlobalConstants.LS_AUTH_STORAGE_KEY);
var nobody = new ClaimsPrincipal(new ClaimsIdentity());
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(nobody)));
}
}

View File

@@ -1,49 +0,0 @@
@using GameIdeas.Resources
@using Blazored.FluentValidation;
<div class="account-setting-container" tabindex="1000">
<div class="account-setting-content @(ContentVisile ? string.Empty : "invisible")">
@if (!IsLogin)
{
<EditForm EditContext="EditContext" OnSubmit="HandleLoginSubmit">
<FluentValidationValidator />
<div class="login-form">
<div class="login-field">
<div class="input-title">@ResourcesKey.EnterUsername</div>
<InputText class="input-text"
@bind-Value="LoginDto.Username" />
</div>
<div class="login-field">
<div class="input-title">@ResourcesKey.EnterPassword</div>
<InputText class="input-text"
@bind-Value="LoginDto.Password" />
</div>
<div class="login-field">
<button class="login-button" type="submit" disabled="@IsLoading">
@if (IsLoading)
{
<div class="loading"></div>
}
else
{
@ResourcesKey.Login
}
</button>
</div>
</div>
</EditForm>
}
else
{
<div class="settings-list">
<div class="settings-element">
@ResourcesKey.UserManager
</div>
<span class="line"></span>
<div class="settings-element" @onclick="HandleLogoutClicked">
@ResourcesKey.Logout
</div>
</div>
}
</div>
</div>

View File

@@ -1,47 +0,0 @@
using GameIdeas.Shared.Dto;
using Microsoft.AspNetCore.Components.Forms;
namespace GameIdeas.BlazorApp.Shared.Components.Account;
public partial class AccountSettings
{
private bool ContentVisile = false;
private EditContext? EditContext;
private LoginDto LoginDto = new();
private bool IsLoading = false;
private bool IsLogin = true;
protected override void OnInitialized()
{
EditContext = new EditContext(LoginDto);
}
public void Close()
{
ContentVisile = false;
StateHasChanged();
}
public void Toggle()
{
ContentVisile = !ContentVisile;
StateHasChanged();
}
private async Task HandleLoginSubmit()
{
if (EditContext?.Validate() == false)
{
return;
}
IsLoading = true;
await Task.Delay(TimeSpan.FromSeconds(5));
Close();
IsLoading = false;
}
private void HandleLogoutClicked()
{
Close();
}
}

View File

@@ -15,4 +15,9 @@ public static class Endpoints
{ {
public static readonly string AllCategories = "api/Category/All"; public static readonly string AllCategories = "api/Category/All";
} }
public static class Auth
{
public static readonly string Login = "api/User/Login";
}
} }

View File

@@ -23,4 +23,8 @@ public static class Icons
public readonly static MarkupString Game = new(OpenBraket + public readonly static MarkupString Game = new(OpenBraket +
"<path d=\"M6,7H18A5,5 0 0,1 23,12A5,5 0 0,1 18,17C16.36,17 14.91,16.21 14,15H10C9.09,16.21 7.64,17 6,17A5,5 0 0,1 1,12A5,5 0 0,1 6,7M19.75,9.5A1.25,1.25 0 0,0 18.5,10.75A1.25,1.25 0 0,0 19.75,12A1.25,1.25 0 0,0 21,10.75A1.25,1.25 0 0,0 19.75,9.5M17.25,12A1.25,1.25 0 0,0 16,13.25A1.25,1.25 0 0,0 17.25,14.5A1.25,1.25 0 0,0 18.5,13.25A1.25,1.25 0 0,0 17.25,12M5,9V11H3V13H5V15H7V13H9V11H7V9H5Z\">" + "<path d=\"M6,7H18A5,5 0 0,1 23,12A5,5 0 0,1 18,17C16.36,17 14.91,16.21 14,15H10C9.09,16.21 7.64,17 6,17A5,5 0 0,1 1,12A5,5 0 0,1 6,7M19.75,9.5A1.25,1.25 0 0,0 18.5,10.75A1.25,1.25 0 0,0 19.75,12A1.25,1.25 0 0,0 21,10.75A1.25,1.25 0 0,0 19.75,9.5M17.25,12A1.25,1.25 0 0,0 16,13.25A1.25,1.25 0 0,0 17.25,14.5A1.25,1.25 0 0,0 18.5,13.25A1.25,1.25 0 0,0 17.25,12M5,9V11H3V13H5V15H7V13H9V11H7V9H5Z\">" +
CloseBraket); CloseBraket);
public readonly static MarkupString Account = new(OpenBraket +
"<path d=\"M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z\" />" +
CloseBraket);
} }

View File

@@ -0,0 +1,3 @@
namespace GameIdeas.BlazorApp.Shared.Exceptions;
public class AuthenticationUserException(string message) : Exception(message);

View File

@@ -11,6 +11,7 @@ public class Translations (TranslationService translationService)
public string EnterUsername => translationService.Translate(nameof(EnterUsername)); public string EnterUsername => translationService.Translate(nameof(EnterUsername));
public string EnterPassword => translationService.Translate(nameof(EnterPassword)); public string EnterPassword => translationService.Translate(nameof(EnterPassword));
public string UserManager => translationService.Translate(nameof(UserManager)); public string UserManager => translationService.Translate(nameof(UserManager));
public string CategoriesManager => translationService.Translate(nameof(CategoriesManager));
public string Filters => translationService.Translate(nameof(Filters)); public string Filters => translationService.Translate(nameof(Filters));
public string LastAdd => translationService.Translate(nameof(LastAdd)); public string LastAdd => translationService.Translate(nameof(LastAdd));
public string Research => translationService.Translate(nameof(Research)); public string Research => translationService.Translate(nameof(Research));
@@ -48,6 +49,11 @@ public class Translations (TranslationService translationService)
public string MinStorageSpaceFormat => translationService.Translate(nameof(MinStorageSpaceFormat)); public string MinStorageSpaceFormat => translationService.Translate(nameof(MinStorageSpaceFormat));
public string MaxStorageSpaceFormat => translationService.Translate(nameof(MaxStorageSpaceFormat)); public string MaxStorageSpaceFormat => translationService.Translate(nameof(MaxStorageSpaceFormat));
public string MinMaxStorageSpaceFormat => translationService.Translate(nameof(MinMaxStorageSpaceFormat)); public string MinMaxStorageSpaceFormat => translationService.Translate(nameof(MinMaxStorageSpaceFormat));
public string UserArgumentsNull => translationService.Translate(nameof(UserArgumentsNull));
public string InvalidToken => translationService.Translate(nameof(InvalidToken));
public string UserUnauthorized => translationService.Translate(nameof(UserUnauthorized));
public string UserLoginFailed => translationService.Translate(nameof(UserLoginFailed));
public string UserLogoutFailed => translationService.Translate(nameof(UserLogoutFailed));
} }
public static class ResourcesKey public static class ResourcesKey
@@ -67,6 +73,7 @@ public static class ResourcesKey
public static string EnterUsername => _instance?.EnterUsername ?? throw new InvalidOperationException("ResourcesKey.EnterUsername is not initialized."); public static string EnterUsername => _instance?.EnterUsername ?? throw new InvalidOperationException("ResourcesKey.EnterUsername is not initialized.");
public static string EnterPassword => _instance?.EnterPassword ?? throw new InvalidOperationException("ResourcesKey.EnterPassword is not initialized."); public static string EnterPassword => _instance?.EnterPassword ?? throw new InvalidOperationException("ResourcesKey.EnterPassword is not initialized.");
public static string UserManager => _instance?.UserManager ?? throw new InvalidOperationException("ResourcesKey.UserManager is not initialized."); public static string UserManager => _instance?.UserManager ?? throw new InvalidOperationException("ResourcesKey.UserManager is not initialized.");
public static string CategoriesManager => _instance?.CategoriesManager ?? throw new InvalidOperationException("ResourcesKey.CategoriesManager is not initialized.");
public static string Filters => _instance?.Filters ?? throw new InvalidOperationException("ResourcesKey.Filters is not initialized."); public static string Filters => _instance?.Filters ?? throw new InvalidOperationException("ResourcesKey.Filters is not initialized.");
public static string LastAdd => _instance?.LastAdd ?? throw new InvalidOperationException("ResourcesKey.LastAdd is not initialized."); public static string LastAdd => _instance?.LastAdd ?? throw new InvalidOperationException("ResourcesKey.LastAdd is not initialized.");
public static string Research => _instance?.Research ?? throw new InvalidOperationException("ResourcesKey.Research is not initialized."); public static string Research => _instance?.Research ?? throw new InvalidOperationException("ResourcesKey.Research is not initialized.");
@@ -104,4 +111,9 @@ public static class ResourcesKey
public static string MinStorageSpaceFormat => _instance?.MinStorageSpaceFormat ?? throw new InvalidOperationException("ResourcesKey.MinStorageSpaceFormat is not initialized."); public static string MinStorageSpaceFormat => _instance?.MinStorageSpaceFormat ?? throw new InvalidOperationException("ResourcesKey.MinStorageSpaceFormat is not initialized.");
public static string MaxStorageSpaceFormat => _instance?.MaxStorageSpaceFormat ?? throw new InvalidOperationException("ResourcesKey.MaxStorageSpaceFormat is not initialized."); public static string MaxStorageSpaceFormat => _instance?.MaxStorageSpaceFormat ?? throw new InvalidOperationException("ResourcesKey.MaxStorageSpaceFormat is not initialized.");
public static string MinMaxStorageSpaceFormat => _instance?.MinMaxStorageSpaceFormat ?? throw new InvalidOperationException("ResourcesKey.MinMaxStorageSpaceFormat is not initialized."); public static string MinMaxStorageSpaceFormat => _instance?.MinMaxStorageSpaceFormat ?? throw new InvalidOperationException("ResourcesKey.MinMaxStorageSpaceFormat is not initialized.");
public static string UserArgumentsNull => _instance?.UserArgumentsNull ?? throw new InvalidOperationException("ResourcesKey.UserArgumentsNull is not initialized.");
public static string InvalidToken => _instance?.InvalidToken ?? throw new InvalidOperationException("ResourcesKey.InvalidToken is not initialized.");
public static string UserUnauthorized => _instance?.UserUnauthorized ?? throw new InvalidOperationException("ResourcesKey.UserUnauthorized is not initialized.");
public static string UserLoginFailed => _instance?.UserLoginFailed ?? throw new InvalidOperationException("ResourcesKey.UserLoginFailed is not initialized.");
public static string UserLogoutFailed => _instance?.UserLogoutFailed ?? throw new InvalidOperationException("ResourcesKey.UserLogoutFailed is not initialized.");
} }

View File

@@ -2,6 +2,18 @@
public class GlobalConstants public class GlobalConstants
{ {
public static int NUMBER_PER_PAGE = 50; public readonly static Guid ADMINISTRATOR_ID = Guid.Parse("{06CA5CB7-6DE5-4A73-9DDD-8E2D5CCDF104}");
public readonly static Guid ADMINISTRATOR_USER_ID = Guid.Parse("{2AB56FCB-0CDE-4DAE-AC9C-FC7635B0D18A}");
public readonly static Guid MEMBER_ID = Guid.Parse("{BCE14DEA-1748-4A76-8485-ADEE83DF5EFD}");
public const string ADMINISTRATOR = "Administrateur";
public const string MEMBER = "Membre";
public const string ADMIN_MEMBER = $"{ADMINISTRATOR}, {MEMBER}";
public const int JWT_DURATION_HOUR = 12;
public const int NUMBER_PER_PAGE = 50;
public const string LS_AUTH_STORAGE_KEY = "authToken";
} }

View File

@@ -6,9 +6,9 @@ public class GameDetailDto
public string? Title { get; set; } public string? Title { get; set; }
public DateTime? ReleaseDate { get; set; } public DateTime? ReleaseDate { get; set; }
public DateTime? CreationDate { get; set; } public DateTime? CreationDate { get; set; }
public int CreationUserId { get; set; } public string CreationUserId { get; set; } = string.Empty;
public DateTime? ModificationDate { get; set; } public DateTime? ModificationDate { get; set; }
public int? ModificationUserId { get; set; } public string? ModificationUserId { get; set; }
public double? StorageSpace { get; set; } public double? StorageSpace { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public int Interest { get; set; } = 3; public int Interest { get; set; } = 3;

View File

@@ -1,11 +0,0 @@
using GameIdeas.Shared.Enum;
namespace GameIdeas.Shared.Dto;
public class LoginDto
{
public int? Id { get; set; }
public string? Username { get; set; }
public string? Password { get; set; }
public Role? Role { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace GameIdeas.Shared.Dto;
public class TokenDto
{
public string? Token { get; set; }
public DateTime? Expiration { get; set; }
}

View File

@@ -7,5 +7,5 @@ public class UserDto
public int? Id { get; set; } public int? Id { get; set; }
public string? Username { get; set; } public string? Username { get; set; }
public string? Password { get; set; } public string? Password { get; set; }
public Role? Role { get; set; } public string? RoleId { get; set; }
} }

View File

@@ -1,8 +0,0 @@
namespace GameIdeas.Shared.Enum;
public enum Role
{
Guest = 1,
Member = 2,
Administrator = 3
}

View File

@@ -6,4 +6,8 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="9.0.4" />
</ItemGroup>
</Project> </Project>

View File

@@ -15,9 +15,9 @@ public partial class Game
public string Title { get; set; } = null!; public string Title { get; set; } = null!;
public DateTime? ReleaseDate { get; set; } public DateTime? ReleaseDate { get; set; }
public DateTime CreationDate { get; set; } public DateTime CreationDate { get; set; }
public int CreationUserId { get; set; } public string CreationUserId { get; set; } = null!;
public DateTime? ModificationDate { get; set; } public DateTime? ModificationDate { get; set; }
public int? ModificationUserId { get; set; } public string? ModificationUserId { get; set; }
public double? StorageSpace { get; set; } public double? StorageSpace { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public int Interest { get; set; } public int Interest { get; set; }

View File

@@ -1,6 +1,8 @@
namespace GameIdeas.Shared.Model; using Microsoft.AspNetCore.Identity;
public partial class User namespace GameIdeas.Shared.Model;
public partial class User : IdentityUser
{ {
public User() public User()
{ {
@@ -8,11 +10,6 @@ public partial class User
ModificationGames = new HashSet<Game>(); ModificationGames = new HashSet<Game>();
} }
public int Id { get; set; }
public string Username { get; set; } = null!;
public string Password { get; set; } = null!;
public int Role { get; set; }
public virtual ICollection<Game> CreationGames { get; set; } public virtual ICollection<Game> CreationGames { get; set; }
public virtual ICollection<Game> ModificationGames { get; set; } public virtual ICollection<Game> ModificationGames { get; set; }
} }

View File

@@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameIdeas.Shared", "GameIde
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Client", "Client", "{9598BBAF-CC9F-4F43-82B2-40F57296C9F0}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Client", "Client", "{9598BBAF-CC9F-4F43-82B2-40F57296C9F0}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameIdeas.WebAPI.Tests", "Server\GameIdeas.WebAPI.Tests\GameIdeas.WebAPI.Tests.csproj", "{D7B46EB2-590D-4A39-90C6-4D553FC4309A}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -37,6 +39,10 @@ Global
{9D6D5C6D-AD66-4353-88CC-638887C42477}.Debug|Any CPU.Build.0 = Debug|Any CPU {9D6D5C6D-AD66-4353-88CC-638887C42477}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9D6D5C6D-AD66-4353-88CC-638887C42477}.Release|Any CPU.ActiveCfg = Release|Any CPU {9D6D5C6D-AD66-4353-88CC-638887C42477}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9D6D5C6D-AD66-4353-88CC-638887C42477}.Release|Any CPU.Build.0 = Release|Any CPU {9D6D5C6D-AD66-4353-88CC-638887C42477}.Release|Any CPU.Build.0 = Release|Any CPU
{D7B46EB2-590D-4A39-90C6-4D553FC4309A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D7B46EB2-590D-4A39-90C6-4D553FC4309A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D7B46EB2-590D-4A39-90C6-4D553FC4309A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7B46EB2-590D-4A39-90C6-4D553FC4309A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -44,6 +50,7 @@ Global
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{ABBADA2F-9017-49A2-AEB9-AC2DB7D70831} = {9598BBAF-CC9F-4F43-82B2-40F57296C9F0} {ABBADA2F-9017-49A2-AEB9-AC2DB7D70831} = {9598BBAF-CC9F-4F43-82B2-40F57296C9F0}
{61C3985E-15DF-4127-9D1F-CAE39F0ADD17} = {F59BED34-9473-436A-A91A-23510A4E0E87} {61C3985E-15DF-4127-9D1F-CAE39F0ADD17} = {F59BED34-9473-436A-A91A-23510A4E0E87}
{D7B46EB2-590D-4A39-90C6-4D553FC4309A} = {F59BED34-9473-436A-A91A-23510A4E0E87}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6380DD77-53E4-4F3B-BB45-FAD2263D1511} SolutionGuid = {6380DD77-53E4-4F3B-BB45-FAD2263D1511}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="9.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="MSTest" Version="3.6.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\GameIdeas.Shared\GameIdeas.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,17 @@
using GameIdeas.Shared.Model;
using Microsoft.AspNetCore.Identity;
namespace GameIdeas.WebAPI.Tests
{
[TestClass]
public sealed class IdentityTest
{
[TestMethod]
public void GetPasswordHash()
{
var hasher = new PasswordHasher<User>();
var hash = hasher.HashPassword(null!, "GameIdeas");
Console.WriteLine(hash);
}
}
}

View File

@@ -0,0 +1 @@
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]

View File

@@ -1,9 +1,10 @@
using GameIdeas.Shared.Model; using GameIdeas.Shared.Model;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace GameIdeas.WebAPI.Context; namespace GameIdeas.WebAPI.Context;
public class GameIdeasContext : DbContext public class GameIdeasContext : IdentityDbContext<User>
{ {
public GameIdeasContext(DbContextOptions<GameIdeasContext> option) public GameIdeasContext(DbContextOptions<GameIdeasContext> option)
: base(option) : base(option)
@@ -12,7 +13,6 @@ public class GameIdeasContext : DbContext
AppContext.SetSwitch("Npgsql.DisableDateTimeInfinityConversions", true); AppContext.SetSwitch("Npgsql.DisableDateTimeInfinityConversions", true);
} }
public virtual DbSet<User> Users { get; set; } = null!;
public virtual DbSet<Developer> Developers { get; set; } = null!; public virtual DbSet<Developer> Developers { get; set; } = null!;
public virtual DbSet<Platform> Platforms { get; set; } = null!; public virtual DbSet<Platform> Platforms { get; set; } = null!;
public virtual DbSet<Property> Properties { get; set; } = null!; public virtual DbSet<Property> Properties { get; set; } = null!;
@@ -27,28 +27,51 @@ public class GameIdeasContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.Entity<User>(entity => { modelBuilder.Entity<Developer>(entity => {
entity.ToTable("User"); entity.ToTable("Developer");
entity.Property(e => e.Id) entity.HasIndex(e => e.Name)
.UseIdentityByDefaultColumn() .IsUnique();
.HasIdentityOptions(startValue: 100000);
}); });
modelBuilder.Entity<Developer>(entity => entity.ToTable("Developer")); modelBuilder.Entity<Platform>(entity => {
entity.ToTable("Platform");
modelBuilder.Entity<Platform>(entity => entity.ToTable("Platform")); entity.HasIndex(e => e.Label)
.IsUnique();
});
modelBuilder.Entity<Property>(entity => entity.ToTable("Property")); modelBuilder.Entity<Property>(entity =>
{
entity.ToTable("Property");
entity.HasIndex(e => e.Label)
.IsUnique();
});
modelBuilder.Entity<Publisher>(entity => entity.ToTable("Publisher")); modelBuilder.Entity<Publisher>(entity =>
{
entity.ToTable("Publisher");
modelBuilder.Entity<Tag>(entity => entity.ToTable("Tag")); entity.HasIndex(e => e.Name)
.IsUnique();
});
modelBuilder.Entity<Tag>(entity =>
{
entity.ToTable("Tag");
entity.HasIndex(e => e.Label)
.IsUnique();
});
modelBuilder.Entity<Game>(entity => modelBuilder.Entity<Game>(entity =>
{ {
entity.ToTable("Game"); entity.ToTable("Game");
entity.HasIndex(e => e.Title)
.IsUnique();
entity.HasIndex(e => e.CreationUserId); entity.HasIndex(e => e.CreationUserId);
entity.HasIndex(e => e.ModificationUserId); entity.HasIndex(e => e.ModificationUserId);

View File

@@ -1,5 +1,7 @@
using GameIdeas.Shared.Dto; using GameIdeas.Shared.Constants;
using GameIdeas.Shared.Dto;
using GameIdeas.WebAPI.Services.Games; using GameIdeas.WebAPI.Services.Games;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace GameIdeas.WebAPI.Controllers; namespace GameIdeas.WebAPI.Controllers;
@@ -42,6 +44,7 @@ public class GameController(
} }
} }
[Authorize(Roles = GlobalConstants.ADMIN_MEMBER)]
[HttpPost("Create")] [HttpPost("Create")]
public async Task<ActionResult<int>> CreateGame([FromBody] GameDetailDto game) public async Task<ActionResult<int>> CreateGame([FromBody] GameDetailDto game)
{ {
@@ -57,6 +60,7 @@ public class GameController(
} }
} }
[Authorize(Roles = GlobalConstants.ADMIN_MEMBER)]
[HttpPut("Update")] [HttpPut("Update")]
public async Task<ActionResult<int>> UpdateGame([FromBody] GameDetailDto game) public async Task<ActionResult<int>> UpdateGame([FromBody] GameDetailDto game)
{ {
@@ -72,6 +76,7 @@ public class GameController(
} }
} }
[Authorize(Roles = GlobalConstants.ADMIN_MEMBER)]
[HttpDelete("Delete/{id:int}")] [HttpDelete("Delete/{id:int}")]
public async Task<ActionResult<bool>> DeleteGame(int id) public async Task<ActionResult<bool>> DeleteGame(int id)
{ {

View File

@@ -0,0 +1,39 @@
using GameIdeas.Shared.Dto;
using GameIdeas.WebAPI.Exceptions;
using GameIdeas.WebAPI.Services.Users;
using Microsoft.AspNetCore.Mvc;
namespace GameIdeas.WebAPI.Controllers;
[ApiController]
[Route("api/[controller]")]
public class UserController(
IUserService userService,
ILoggerFactory loggerFactory) : Controller
{
private readonly ILogger<UserController> logger = loggerFactory.CreateLogger<UserController>();
[HttpPost("Login")]
public async Task<ActionResult<TokenDto>> Login([FromBody] UserDto model)
{
try
{
return Ok(await userService.Login(model));
}
catch (UserInvalidException e)
{
logger.LogInformation(e, "Missing informations for authentication");
return StatusCode(406, e.Message);
}
catch (UserUnauthorizedException e)
{
logger.LogWarning(e, "Authentication invalid with there informations");
return Unauthorized(e.Message);
}
catch (Exception e)
{
logger.LogError(e, "Internal error while search games");
return StatusCode(500, e.Message);
}
}
}

View File

@@ -0,0 +1,3 @@
namespace GameIdeas.WebAPI.Exceptions;
public class UserInvalidException (string message) : Exception(message);

View File

@@ -0,0 +1,3 @@
namespace GameIdeas.WebAPI.Exceptions;
public class UserUnauthorizedException(string message) : Exception(message);

View File

@@ -7,6 +7,7 @@
"EnterUsername": "Nom d'utilisateur", "EnterUsername": "Nom d'utilisateur",
"EnterPassword": "Mot de passe", "EnterPassword": "Mot de passe",
"UserManager": "Gestion des utilisateurs", "UserManager": "Gestion des utilisateurs",
"CategoriesManager": "Gestion des catégories",
"Filters": "Les filtres", "Filters": "Les filtres",
"LastAdd": "Les ajouts récents", "LastAdd": "Les ajouts récents",
"Research": "Rechercher", "Research": "Rechercher",
@@ -43,5 +44,10 @@
"ErrorStorageSpaceLabel": "Erreur lors de la génération des label de l'espace de stockage", "ErrorStorageSpaceLabel": "Erreur lors de la génération des label de l'espace de stockage",
"MinStorageSpaceFormat": "Jusqu'à {0}", "MinStorageSpaceFormat": "Jusqu'à {0}",
"MaxStorageSpaceFormat": "Plus de {0}", "MaxStorageSpaceFormat": "Plus de {0}",
"MinMaxStorageSpaceFormat": "{0} à {1}" "MinMaxStorageSpaceFormat": "{0} à {1}",
"UserArgumentsNull": "Nom d'utilisateur ou mot de passe invalide",
"InvalidToken": "Le token JWT est invalide",
"UserUnauthorized": "Utilisateur non authorisé",
"UserLoginFailed": "Authentification de l'utilisateur échoué",
"UserLogoutFailed": "Déconnection de l'utilisateur échoué"
} }

View File

@@ -4,6 +4,7 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>c5ccfd3a-f458-4660-b6c4-81fcc2513737</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -12,6 +13,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="14.0.0" /> <PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.4">

View File

@@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace GameIdeas.WebAPI.Migrations namespace GameIdeas.WebAPI.Migrations
{ {
[DbContext(typeof(GameIdeasContext))] [DbContext(typeof(GameIdeasContext))]
[Migration("20250409225125_InitialCreate")] [Migration("20250420153030_InitialCreate")]
partial class InitialCreate partial class InitialCreate
{ {
/// <inheritdoc /> /// <inheritdoc />
@@ -39,6 +39,9 @@ namespace GameIdeas.WebAPI.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Developer", (string)null); b.ToTable("Developer", (string)null);
}); });
@@ -53,11 +56,12 @@ namespace GameIdeas.WebAPI.Migrations
b.Property<DateTime>("CreationDate") b.Property<DateTime>("CreationDate")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp without time zone")
.HasDefaultValueSql("now()"); .HasDefaultValueSql("now()");
b.Property<int>("CreationUserId") b.Property<string>("CreationUserId")
.HasColumnType("integer"); .IsRequired()
.HasColumnType("text");
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("text"); .HasColumnType("text");
@@ -66,13 +70,13 @@ namespace GameIdeas.WebAPI.Migrations
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<DateTime?>("ModificationDate") b.Property<DateTime?>("ModificationDate")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp without time zone");
b.Property<int?>("ModificationUserId") b.Property<string>("ModificationUserId")
.HasColumnType("integer"); .HasColumnType("text");
b.Property<DateTime?>("ReleaseDate") b.Property<DateTime?>("ReleaseDate")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp without time zone");
b.Property<double?>("StorageSpace") b.Property<double?>("StorageSpace")
.HasColumnType("double precision"); .HasColumnType("double precision");
@@ -87,6 +91,9 @@ namespace GameIdeas.WebAPI.Migrations
b.HasIndex("ModificationUserId"); b.HasIndex("ModificationUserId");
b.HasIndex("Title")
.IsUnique();
b.ToTable("Game", (string)null); b.ToTable("Game", (string)null);
}); });
@@ -182,6 +189,9 @@ namespace GameIdeas.WebAPI.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Label")
.IsUnique();
b.ToTable("Platform", (string)null); b.ToTable("Platform", (string)null);
}); });
@@ -199,6 +209,9 @@ namespace GameIdeas.WebAPI.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Label")
.IsUnique();
b.ToTable("Property", (string)null); b.ToTable("Property", (string)null);
}); });
@@ -216,6 +229,9 @@ namespace GameIdeas.WebAPI.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Publisher", (string)null); b.ToTable("Publisher", (string)null);
}); });
@@ -233,32 +249,206 @@ namespace GameIdeas.WebAPI.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Label")
.IsUnique();
b.ToTable("Tag", (string)null); b.ToTable("Tag", (string)null);
}); });
modelBuilder.Entity("GameIdeas.Shared.Model.User", b => modelBuilder.Entity("GameIdeas.Shared.Model.User", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer"); .HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
NpgsqlPropertyBuilderExtensions.HasIdentityOptions(b.Property<int>("Id"), 100000L, null, null, null, null, null);
b.Property<string>("Password") b.Property<string>("ClaimType")
.IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<int>("Role") b.Property<string>("ClaimValue")
.HasColumnType("integer"); .HasColumnType("text");
b.Property<string>("Username") b.Property<string>("RoleId")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("User", (string)null); b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
}); });
modelBuilder.Entity("GameIdeas.Shared.Model.Game", b => modelBuilder.Entity("GameIdeas.Shared.Model.Game", b =>
@@ -372,6 +562,57 @@ namespace GameIdeas.WebAPI.Migrations
b.Navigation("Tag"); b.Navigation("Tag");
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("GameIdeas.Shared.Model.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("GameIdeas.Shared.Model.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GameIdeas.Shared.Model.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("GameIdeas.Shared.Model.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("GameIdeas.Shared.Model.Developer", b => modelBuilder.Entity("GameIdeas.Shared.Model.Developer", b =>
{ {
b.Navigation("GameDevelopers"); b.Navigation("GameDevelopers");

View File

@@ -12,6 +12,45 @@ namespace GameIdeas.WebAPI.Migrations
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: true),
SecurityStamp = table.Column<string>(type: "text", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
PhoneNumber = table.Column<string>(type: "text", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Developer", name: "Developer",
columns: table => new columns: table => new
@@ -78,19 +117,109 @@ namespace GameIdeas.WebAPI.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "User", name: "AspNetRoleClaims",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(type: "integer", nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:IdentitySequenceOptions", "'100000', '1', '', '', 'False', '1'")
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Username = table.Column<string>(type: "text", nullable: false), RoleId = table.Column<string>(type: "text", nullable: false),
Password = table.Column<string>(type: "text", nullable: false), ClaimType = table.Column<string>(type: "text", nullable: true),
Role = table.Column<int>(type: "integer", nullable: false) ClaimValue = table.Column<string>(type: "text", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_User", x => x.Id); table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<string>(type: "text", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(type: "text", nullable: false),
ProviderKey = table.Column<string>(type: "text", nullable: false),
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
UserId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
table.ForeignKey(
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<string>(type: "text", nullable: false),
RoleId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<string>(type: "text", nullable: false),
LoginProvider = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Value = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
table.ForeignKey(
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@@ -101,11 +230,11 @@ namespace GameIdeas.WebAPI.Migrations
.Annotation("Npgsql:IdentitySequenceOptions", "'100000', '1', '', '', 'False', '1'") .Annotation("Npgsql:IdentitySequenceOptions", "'100000', '1', '', '', 'False', '1'")
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Title = table.Column<string>(type: "text", nullable: false), Title = table.Column<string>(type: "text", nullable: false),
ReleaseDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true), ReleaseDate = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"), CreationDate = table.Column<DateTime>(type: "timestamp without time zone", nullable: false, defaultValueSql: "now()"),
CreationUserId = table.Column<int>(type: "integer", nullable: false), CreationUserId = table.Column<string>(type: "text", nullable: false),
ModificationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true), ModificationDate = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
ModificationUserId = table.Column<int>(type: "integer", nullable: true), ModificationUserId = table.Column<string>(type: "text", nullable: true),
StorageSpace = table.Column<double>(type: "double precision", nullable: true), StorageSpace = table.Column<double>(type: "double precision", nullable: true),
Description = table.Column<string>(type: "text", nullable: true), Description = table.Column<string>(type: "text", nullable: true),
Interest = table.Column<int>(type: "integer", nullable: false) Interest = table.Column<int>(type: "integer", nullable: false)
@@ -114,14 +243,14 @@ namespace GameIdeas.WebAPI.Migrations
{ {
table.PrimaryKey("PK_Game", x => x.Id); table.PrimaryKey("PK_Game", x => x.Id);
table.ForeignKey( table.ForeignKey(
name: "FK_Game_User_CreationUserId", name: "FK_Game_AspNetUsers_CreationUserId",
column: x => x.CreationUserId, column: x => x.CreationUserId,
principalTable: "User", principalTable: "AspNetUsers",
principalColumn: "Id"); principalColumn: "Id");
table.ForeignKey( table.ForeignKey(
name: "FK_Game_User_ModificationUserId", name: "FK_Game_AspNetUsers_ModificationUserId",
column: x => x.ModificationUserId, column: x => x.ModificationUserId,
principalTable: "User", principalTable: "AspNetUsers",
principalColumn: "Id"); principalColumn: "Id");
}); });
@@ -246,6 +375,49 @@ namespace GameIdeas.WebAPI.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "AspNetRoles",
column: "NormalizedName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AspNetUserClaims_UserId",
table: "AspNetUserClaims",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserLogins_UserId",
table: "AspNetUserLogins",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserRoles_RoleId",
table: "AspNetUserRoles",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "EmailIndex",
table: "AspNetUsers",
column: "NormalizedEmail");
migrationBuilder.CreateIndex(
name: "UserNameIndex",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Developer_Name",
table: "Developer",
column: "Name",
unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Game_CreationUserId", name: "IX_Game_CreationUserId",
table: "Game", table: "Game",
@@ -256,6 +428,12 @@ namespace GameIdeas.WebAPI.Migrations
table: "Game", table: "Game",
column: "ModificationUserId"); column: "ModificationUserId");
migrationBuilder.CreateIndex(
name: "IX_Game_Title",
table: "Game",
column: "Title",
unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_GameDeveloper_DeveloperId", name: "IX_GameDeveloper_DeveloperId",
table: "GameDeveloper", table: "GameDeveloper",
@@ -280,11 +458,50 @@ namespace GameIdeas.WebAPI.Migrations
name: "IX_GameTag_TagId", name: "IX_GameTag_TagId",
table: "GameTag", table: "GameTag",
column: "TagId"); column: "TagId");
migrationBuilder.CreateIndex(
name: "IX_Platform_Label",
table: "Platform",
column: "Label",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Property_Label",
table: "Property",
column: "Label",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Publisher_Name",
table: "Publisher",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Tag_Label",
table: "Tag",
column: "Label",
unique: true);
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable(
name: "AspNetRoleClaims");
migrationBuilder.DropTable(
name: "AspNetUserClaims");
migrationBuilder.DropTable(
name: "AspNetUserLogins");
migrationBuilder.DropTable(
name: "AspNetUserRoles");
migrationBuilder.DropTable(
name: "AspNetUserTokens");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "GameDeveloper"); name: "GameDeveloper");
@@ -300,6 +517,9 @@ namespace GameIdeas.WebAPI.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "GameTag"); name: "GameTag");
migrationBuilder.DropTable(
name: "AspNetRoles");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Developer"); name: "Developer");
@@ -319,7 +539,7 @@ namespace GameIdeas.WebAPI.Migrations
name: "Tag"); name: "Tag");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "User"); name: "AspNetUsers");
} }
} }
} }

View File

@@ -0,0 +1,663 @@
// <auto-generated />
using System;
using GameIdeas.WebAPI.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace GameIdeas.WebAPI.Migrations
{
[DbContext(typeof(GameIdeasContext))]
[Migration("20250420160158_SeedDefaultUser")]
partial class SeedDefaultUser
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("GameIdeas.Shared.Model.Developer", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Developer", (string)null);
});
modelBuilder.Entity("GameIdeas.Shared.Model.Game", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
NpgsqlPropertyBuilderExtensions.HasIdentityOptions(b.Property<int>("Id"), 100000L, null, null, null, null, null);
b.Property<DateTime>("CreationDate")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp without time zone")
.HasDefaultValueSql("now()");
b.Property<string>("CreationUserId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<int>("Interest")
.HasColumnType("integer");
b.Property<DateTime?>("ModificationDate")
.HasColumnType("timestamp without time zone");
b.Property<string>("ModificationUserId")
.HasColumnType("text");
b.Property<DateTime?>("ReleaseDate")
.HasColumnType("timestamp without time zone");
b.Property<double?>("StorageSpace")
.HasColumnType("double precision");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreationUserId");
b.HasIndex("ModificationUserId");
b.HasIndex("Title")
.IsUnique();
b.ToTable("Game", (string)null);
});
modelBuilder.Entity("GameIdeas.Shared.Model.GameDeveloper", b =>
{
b.Property<int>("GameId")
.HasColumnType("integer");
b.Property<int>("DeveloperId")
.HasColumnType("integer");
b.HasKey("GameId", "DeveloperId");
b.HasIndex("DeveloperId");
b.ToTable("GameDeveloper", (string)null);
});
modelBuilder.Entity("GameIdeas.Shared.Model.GamePlatform", b =>
{
b.Property<int>("GameId")
.HasColumnType("integer");
b.Property<int>("PlatformId")
.HasColumnType("integer");
b.Property<string>("Url")
.HasColumnType("text");
b.HasKey("GameId", "PlatformId");
b.HasIndex("PlatformId");
b.ToTable("GamePlatform", (string)null);
});
modelBuilder.Entity("GameIdeas.Shared.Model.GameProperty", b =>
{
b.Property<int>("GameId")
.HasColumnType("integer");
b.Property<int>("PropertyId")
.HasColumnType("integer");
b.HasKey("GameId", "PropertyId");
b.HasIndex("PropertyId");
b.ToTable("GameProperty", (string)null);
});
modelBuilder.Entity("GameIdeas.Shared.Model.GamePublisher", b =>
{
b.Property<int>("GameId")
.HasColumnType("integer");
b.Property<int>("PublisherId")
.HasColumnType("integer");
b.HasKey("GameId", "PublisherId");
b.HasIndex("PublisherId");
b.ToTable("GamePublisher", (string)null);
});
modelBuilder.Entity("GameIdeas.Shared.Model.GameTag", b =>
{
b.Property<int>("GameId")
.HasColumnType("integer");
b.Property<int>("TagId")
.HasColumnType("integer");
b.HasKey("GameId", "TagId");
b.HasIndex("TagId");
b.ToTable("GameTag", (string)null);
});
modelBuilder.Entity("GameIdeas.Shared.Model.Platform", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Label")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Label")
.IsUnique();
b.ToTable("Platform", (string)null);
});
modelBuilder.Entity("GameIdeas.Shared.Model.Property", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Label")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Label")
.IsUnique();
b.ToTable("Property", (string)null);
});
modelBuilder.Entity("GameIdeas.Shared.Model.Publisher", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Publisher", (string)null);
});
modelBuilder.Entity("GameIdeas.Shared.Model.Tag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Label")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Label")
.IsUnique();
b.ToTable("Tag", (string)null);
});
modelBuilder.Entity("GameIdeas.Shared.Model.User", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("GameIdeas.Shared.Model.Game", b =>
{
b.HasOne("GameIdeas.Shared.Model.User", "CreationUser")
.WithMany("CreationGames")
.HasForeignKey("CreationUserId")
.IsRequired();
b.HasOne("GameIdeas.Shared.Model.User", "ModificationUser")
.WithMany("ModificationGames")
.HasForeignKey("ModificationUserId");
b.Navigation("CreationUser");
b.Navigation("ModificationUser");
});
modelBuilder.Entity("GameIdeas.Shared.Model.GameDeveloper", b =>
{
b.HasOne("GameIdeas.Shared.Model.Developer", "Developer")
.WithMany("GameDevelopers")
.HasForeignKey("DeveloperId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GameIdeas.Shared.Model.Game", "Game")
.WithMany("GameDevelopers")
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Developer");
b.Navigation("Game");
});
modelBuilder.Entity("GameIdeas.Shared.Model.GamePlatform", b =>
{
b.HasOne("GameIdeas.Shared.Model.Game", "Game")
.WithMany("GamePlatforms")
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GameIdeas.Shared.Model.Platform", "Platform")
.WithMany("GamePlatforms")
.HasForeignKey("PlatformId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Game");
b.Navigation("Platform");
});
modelBuilder.Entity("GameIdeas.Shared.Model.GameProperty", b =>
{
b.HasOne("GameIdeas.Shared.Model.Game", "Game")
.WithMany("GameProperties")
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GameIdeas.Shared.Model.Property", "Property")
.WithMany("GameProperties")
.HasForeignKey("PropertyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Game");
b.Navigation("Property");
});
modelBuilder.Entity("GameIdeas.Shared.Model.GamePublisher", b =>
{
b.HasOne("GameIdeas.Shared.Model.Game", "Game")
.WithMany("GamePublishers")
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GameIdeas.Shared.Model.Publisher", "Publisher")
.WithMany("GamePublishers")
.HasForeignKey("PublisherId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Game");
b.Navigation("Publisher");
});
modelBuilder.Entity("GameIdeas.Shared.Model.GameTag", b =>
{
b.HasOne("GameIdeas.Shared.Model.Game", "Game")
.WithMany("GameTags")
.HasForeignKey("GameId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GameIdeas.Shared.Model.Tag", "Tag")
.WithMany("GameTags")
.HasForeignKey("TagId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Game");
b.Navigation("Tag");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("GameIdeas.Shared.Model.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("GameIdeas.Shared.Model.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GameIdeas.Shared.Model.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("GameIdeas.Shared.Model.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("GameIdeas.Shared.Model.Developer", b =>
{
b.Navigation("GameDevelopers");
});
modelBuilder.Entity("GameIdeas.Shared.Model.Game", b =>
{
b.Navigation("GameDevelopers");
b.Navigation("GamePlatforms");
b.Navigation("GameProperties");
b.Navigation("GamePublishers");
b.Navigation("GameTags");
});
modelBuilder.Entity("GameIdeas.Shared.Model.Platform", b =>
{
b.Navigation("GamePlatforms");
});
modelBuilder.Entity("GameIdeas.Shared.Model.Property", b =>
{
b.Navigation("GameProperties");
});
modelBuilder.Entity("GameIdeas.Shared.Model.Publisher", b =>
{
b.Navigation("GamePublishers");
});
modelBuilder.Entity("GameIdeas.Shared.Model.Tag", b =>
{
b.Navigation("GameTags");
});
modelBuilder.Entity("GameIdeas.Shared.Model.User", b =>
{
b.Navigation("CreationGames");
b.Navigation("ModificationGames");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,63 @@
using GameIdeas.Shared.Constants;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GameIdeas.WebAPI.Migrations
{
/// <inheritdoc />
public partial class SeedDefaultUser : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.InsertData(
table: "AspNetRoles",
columns: ["Id", "Name", "NormalizedName", "ConcurrencyStamp"],
values: new object[,]
{
{
GlobalConstants.ADMINISTRATOR_ID.ToString(),
GlobalConstants.ADMINISTRATOR,
GlobalConstants.ADMINISTRATOR.Normalize(),
Guid.NewGuid().ToString()
},
{
GlobalConstants.MEMBER_ID.ToString(),
GlobalConstants.MEMBER,
GlobalConstants.MEMBER.Normalize(),
Guid.NewGuid().ToString()
}
});
migrationBuilder.InsertData(
table: "AspNetUsers",
columns:
[
"Id", "UserName", "NormalizedUserName", "EmailConfirmed", "PhoneNumberConfirmed", "TwoFactorEnabled"
, "PasswordHash", "SecurityStamp", "ConcurrencyStamp", "AccessFailedCount", "LockoutEnabled"
],
values:
[
GlobalConstants.ADMINISTRATOR_USER_ID.ToString(), "admin", "ADMIN",
false, false, false,
"AQAAAAIAAYagAAAAEOGx7MFBLpS7awda0ww6jsfXsnhsUjYd4gDK9DaGvQv0X9UZTuHStr5v5+t4Y1S+xg==",
Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), 0, false
]);
migrationBuilder.InsertData(
table: "AspNetUserRoles",
columns: ["UserId", "RoleId"],
values: [GlobalConstants.ADMINISTRATOR_USER_ID.ToString(), GlobalConstants.ADMINISTRATOR_ID.ToString()]);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@$"DELETE FROM ""AspNetUserRoles"" WHERE ""UserId"" = '{GlobalConstants.ADMINISTRATOR_USER_ID.ToString()}' AND ""RoleId"" = '{GlobalConstants.ADMINISTRATOR_ID.ToString()}'");
migrationBuilder.DeleteData("AspNetUsers", "Id", GlobalConstants.ADMINISTRATOR_USER_ID.ToString());
migrationBuilder.DeleteData("AspNetRoles", "Id", GlobalConstants.ADMINISTRATOR_ID.ToString());
migrationBuilder.DeleteData("AspNetRoles", "Id", GlobalConstants.MEMBER_ID.ToString());
}
}
}

View File

@@ -36,6 +36,9 @@ namespace GameIdeas.WebAPI.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Developer", (string)null); b.ToTable("Developer", (string)null);
}); });
@@ -50,11 +53,12 @@ namespace GameIdeas.WebAPI.Migrations
b.Property<DateTime>("CreationDate") b.Property<DateTime>("CreationDate")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp without time zone")
.HasDefaultValueSql("now()"); .HasDefaultValueSql("now()");
b.Property<int>("CreationUserId") b.Property<string>("CreationUserId")
.HasColumnType("integer"); .IsRequired()
.HasColumnType("text");
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("text"); .HasColumnType("text");
@@ -63,13 +67,13 @@ namespace GameIdeas.WebAPI.Migrations
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<DateTime?>("ModificationDate") b.Property<DateTime?>("ModificationDate")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp without time zone");
b.Property<int?>("ModificationUserId") b.Property<string>("ModificationUserId")
.HasColumnType("integer"); .HasColumnType("text");
b.Property<DateTime?>("ReleaseDate") b.Property<DateTime?>("ReleaseDate")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp without time zone");
b.Property<double?>("StorageSpace") b.Property<double?>("StorageSpace")
.HasColumnType("double precision"); .HasColumnType("double precision");
@@ -84,6 +88,9 @@ namespace GameIdeas.WebAPI.Migrations
b.HasIndex("ModificationUserId"); b.HasIndex("ModificationUserId");
b.HasIndex("Title")
.IsUnique();
b.ToTable("Game", (string)null); b.ToTable("Game", (string)null);
}); });
@@ -179,6 +186,9 @@ namespace GameIdeas.WebAPI.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Label")
.IsUnique();
b.ToTable("Platform", (string)null); b.ToTable("Platform", (string)null);
}); });
@@ -196,6 +206,9 @@ namespace GameIdeas.WebAPI.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Label")
.IsUnique();
b.ToTable("Property", (string)null); b.ToTable("Property", (string)null);
}); });
@@ -213,6 +226,9 @@ namespace GameIdeas.WebAPI.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Publisher", (string)null); b.ToTable("Publisher", (string)null);
}); });
@@ -230,32 +246,206 @@ namespace GameIdeas.WebAPI.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("Label")
.IsUnique();
b.ToTable("Tag", (string)null); b.ToTable("Tag", (string)null);
}); });
modelBuilder.Entity("GameIdeas.Shared.Model.User", b => modelBuilder.Entity("GameIdeas.Shared.Model.User", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer"); .HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
NpgsqlPropertyBuilderExtensions.HasIdentityOptions(b.Property<int>("Id"), 100000L, null, null, null, null, null);
b.Property<string>("Password") b.Property<string>("ClaimType")
.IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<int>("Role") b.Property<string>("ClaimValue")
.HasColumnType("integer"); .HasColumnType("text");
b.Property<string>("Username") b.Property<string>("RoleId")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("User", (string)null); b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
}); });
modelBuilder.Entity("GameIdeas.Shared.Model.Game", b => modelBuilder.Entity("GameIdeas.Shared.Model.Game", b =>
@@ -369,6 +559,57 @@ namespace GameIdeas.WebAPI.Migrations
b.Navigation("Tag"); b.Navigation("Tag");
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("GameIdeas.Shared.Model.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("GameIdeas.Shared.Model.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GameIdeas.Shared.Model.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("GameIdeas.Shared.Model.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("GameIdeas.Shared.Model.Developer", b => modelBuilder.Entity("GameIdeas.Shared.Model.Developer", b =>
{ {
b.Navigation("GameDevelopers"); b.Navigation("GameDevelopers");

View File

@@ -1,20 +0,0 @@
using AutoMapper;
using GameIdeas.Shared.Dto;
using GameIdeas.Shared.Enum;
using GameIdeas.Shared.Model;
namespace GameIdeas.WebAPI.Profiles;
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
.ForMember(d => d.Username, o => o.MapFrom(s => s.Username))
.ForMember(d => d.Password, o => o.MapFrom(s => s.Password))
.ForMember(d => d.Role, o => o.MapFrom(s => s.Role))
.ReverseMap();
}
}

View File

@@ -1,8 +1,14 @@
using GameIdeas.Resources; using GameIdeas.Resources;
using GameIdeas.Shared.Model;
using GameIdeas.WebAPI.Context; using GameIdeas.WebAPI.Context;
using GameIdeas.WebAPI.Services.Categories; using GameIdeas.WebAPI.Services.Categories;
using GameIdeas.WebAPI.Services.Games; using GameIdeas.WebAPI.Services.Games;
using GameIdeas.WebAPI.Services.Users;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
var services = builder.Services; var services = builder.Services;
@@ -27,9 +33,38 @@ Action<DbContextOptionsBuilder> dbContextOptions = options =>
// Add services to the container. // Add services to the container.
services.AddDbContext<GameIdeasContext>(dbContextOptions); services.AddDbContext<GameIdeasContext>(dbContextOptions);
services.AddIdentity<User, IdentityRole>()
.AddEntityFrameworkStores<GameIdeasContext>()
.AddDefaultTokenProviders();
var jwtKey = Environment.GetEnvironmentVariable("JWT_KEY")
?? throw new ArgumentNullException(message: "Invalid key for JWT token", null);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Environment.GetEnvironmentVariable("JWT_ISSUER"),
ValidAudience = Environment.GetEnvironmentVariable("JWT_AUDIENCE"),
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
});
services.AddAuthorization();
services.AddSingleton<TranslationService>(); services.AddSingleton<TranslationService>();
services.AddSingleton<Translations>(); services.AddSingleton<Translations>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IGameReadService, GameReadService>(); services.AddScoped<IGameReadService, GameReadService>();
services.AddScoped<IGameWriteService, GameWriteService>(); services.AddScoped<IGameWriteService, GameWriteService>();
services.AddScoped<ICategoryService, CategoryService>(); services.AddScoped<ICategoryService, CategoryService>();

View File

@@ -0,0 +1,8 @@
using GameIdeas.Shared.Dto;
namespace GameIdeas.WebAPI.Services.Users;
public interface IUserService
{
Task<TokenDto> Login(UserDto user);
}

View File

@@ -0,0 +1,57 @@
using GameIdeas.Resources;
using GameIdeas.Shared.Constants;
using GameIdeas.Shared.Dto;
using GameIdeas.Shared.Model;
using GameIdeas.WebAPI.Exceptions;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace GameIdeas.WebAPI.Services.Users;
public class UserService(UserManager<User> userManager) : IUserService
{
public async Task<TokenDto> Login(UserDto userDto)
{
if (userDto.Username == null || userDto.Password == null)
throw new UserInvalidException(ResourcesKey.UserArgumentsNull);
var user = await userManager.FindByNameAsync(userDto.Username);
if (user == null || !await userManager.CheckPasswordAsync(user, userDto.Password))
{
throw new UserUnauthorizedException(ResourcesKey.UserUnauthorized);
}
List<Claim> authClaims =
[
new Claim(ClaimTypes.Name, user.UserName ?? string.Empty),
new Claim(ClaimTypes.Sid, user.Id),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
];
authClaims.AddRange((await userManager.GetRolesAsync(user))
.Select(r => new Claim(ClaimTypes.Role, r)));
var jwtKey = Environment.GetEnvironmentVariable("JWT_KEY")
?? throw new ArgumentNullException(message: ResourcesKey.InvalidToken, null);
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
var token = new JwtSecurityToken(
issuer: Environment.GetEnvironmentVariable("JWT_ISSUER"),
audience: Environment.GetEnvironmentVariable("JWT_AUDIENCE"),
expires: DateTime.Now.AddHours(GlobalConstants.JWT_DURATION_HOUR),
claims: authClaims,
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
);
return new TokenDto
{
Token = new JwtSecurityTokenHandler().WriteToken(token),
Expiration = token.ValidTo
};
}
}