auth changes

This commit is contained in:
2024-03-28 19:17:46 +01:00
Unverified
parent 0d9a42dd24
commit fcca2119a5
45 changed files with 6588 additions and 55 deletions

View File

@@ -22,8 +22,8 @@ namespace WatchIt.Database.Model.Account
public long Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string Description { get; set; }
public short GenderId { get; set; }
public string? Description { get; set; }
public short? GenderId { get; set; }
public Guid? ProfilePictureId { get; set; }
public Guid? BackgroundPictureId { get; set; }
public byte[] Password { get; set; }
@@ -49,6 +49,8 @@ namespace WatchIt.Database.Model.Account
public IEnumerable<RatingMediaSeriesSeason> RatingMediaSeriesSeason { get; set; }
public IEnumerable<RatingMediaSeriesEpisode> RatingMediaSeriesEpisode { get; set; }
public IEnumerable<AccountRefreshToken> AccountRefreshTokens { get; set; }
#endregion
@@ -76,10 +78,8 @@ namespace WatchIt.Database.Model.Account
builder.HasOne(x => x.Gender)
.WithMany()
.HasForeignKey(x => x.GenderId)
.IsRequired();
builder.Property(x => x.GenderId)
.IsRequired();
.HasForeignKey(x => x.GenderId);
builder.Property(x => x.GenderId);
builder.HasOne(x => x.ProfilePicture)
.WithOne(x => x.Account)
@@ -104,7 +104,8 @@ namespace WatchIt.Database.Model.Account
.IsRequired();
builder.Property(x => x.IsAdmin)
.IsRequired();
.IsRequired()
.HasDefaultValue(false);
builder.Property(x => x.CreationDate)
.IsRequired()

View File

@@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WatchIt.Database.Model.Account
{
public class AccountRefreshToken : IEntity<AccountRefreshToken>
{
#region PROPERTIES
public Guid Id { get; set; }
public long AccountId { get; set; }
public DateTime ExpirationDate { get; set; }
public bool IsExtendable { get; set; }
#endregion
#region NAVIGATION
public Account Account { get; set; }
#endregion
#region PUBLIC METHODS
static void IEntity<AccountRefreshToken>.Build(EntityTypeBuilder<AccountRefreshToken> builder)
{
builder.HasKey(x => x.Id);
builder.HasIndex(x => x.Id)
.IsUnique();
builder.Property(x => x.Id)
.IsRequired();
builder.HasOne(x => x.Account)
.WithMany(x => x.AccountRefreshTokens)
.HasForeignKey(x => x.AccountId)
.IsRequired();
builder.Property(x => x.AccountId)
.IsRequired();
builder.Property(x => x.ExpirationDate)
.IsRequired();
builder.Property(x => x.IsExtendable)
.IsRequired();
}
#endregion
}
}

View File

@@ -37,6 +37,7 @@ namespace WatchIt.Database
// Account
public virtual DbSet<Account> Accounts { get; set; }
public virtual DbSet<AccountProfilePicture> AccountProfilePictures { get; set; }
public virtual DbSet<AccountRefreshToken> AccountRefreshTokens { get; set; }
// Media
public virtual DbSet<Media> Media { get; set; }
@@ -87,6 +88,7 @@ namespace WatchIt.Database
// Account
EntityBuilder.Build<Account>(modelBuilder);
EntityBuilder.Build<AccountProfilePicture>(modelBuilder);
EntityBuilder.Build<AccountRefreshToken>(modelBuilder);
// Media
EntityBuilder.Build<Media>(modelBuilder);

View File

@@ -0,0 +1,77 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace WatchIt.Database.Migrations
{
/// <inheritdoc />
public partial class _0003_GenderNotRequiredAndDefaultNotAdmin : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Accounts_Genders_GenderId",
table: "Accounts");
migrationBuilder.AlterColumn<bool>(
name: "IsAdmin",
table: "Accounts",
type: "boolean",
nullable: false,
defaultValue: false,
oldClrType: typeof(bool),
oldType: "boolean");
migrationBuilder.AlterColumn<short>(
name: "GenderId",
table: "Accounts",
type: "smallint",
nullable: true,
oldClrType: typeof(short),
oldType: "smallint");
migrationBuilder.AddForeignKey(
name: "FK_Accounts_Genders_GenderId",
table: "Accounts",
column: "GenderId",
principalTable: "Genders",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Accounts_Genders_GenderId",
table: "Accounts");
migrationBuilder.AlterColumn<bool>(
name: "IsAdmin",
table: "Accounts",
type: "boolean",
nullable: false,
oldClrType: typeof(bool),
oldType: "boolean",
oldDefaultValue: false);
migrationBuilder.AlterColumn<short>(
name: "GenderId",
table: "Accounts",
type: "smallint",
nullable: false,
defaultValue: (short)0,
oldClrType: typeof(short),
oldType: "smallint",
oldNullable: true);
migrationBuilder.AddForeignKey(
name: "FK_Accounts_Genders_GenderId",
table: "Accounts",
column: "GenderId",
principalTable: "Genders",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace WatchIt.Database.Migrations
{
/// <inheritdoc />
public partial class _0004_AccountDescriptionNullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Description",
table: "Accounts",
type: "character varying(1000)",
maxLength: 1000,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(1000)",
oldMaxLength: 1000);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Description",
table: "Accounts",
type: "character varying(1000)",
maxLength: 1000,
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "character varying(1000)",
oldMaxLength: 1000,
oldNullable: true);
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace WatchIt.Database.Migrations
{
/// <inheritdoc />
public partial class _0005_AccountRefreshTokensAdded : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AccountRefreshTokens",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
AccountId = table.Column<long>(type: "bigint", nullable: false),
Lifetime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AccountRefreshTokens", x => x.Id);
table.ForeignKey(
name: "FK_AccountRefreshTokens_Accounts_AccountId",
column: x => x.AccountId,
principalTable: "Accounts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AccountRefreshTokens_AccountId",
table: "AccountRefreshTokens",
column: "AccountId");
migrationBuilder.CreateIndex(
name: "IX_AccountRefreshTokens_Id",
table: "AccountRefreshTokens",
column: "Id",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AccountRefreshTokens");
}
}
}

View File

@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace WatchIt.Database.Migrations
{
/// <inheritdoc />
public partial class _0006_AccountRefreshTokenChanges : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Lifetime",
table: "AccountRefreshTokens",
newName: "ExpirationDate");
migrationBuilder.AddColumn<bool>(
name: "IsExtendable",
table: "AccountRefreshTokens",
type: "boolean",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsExtendable",
table: "AccountRefreshTokens");
migrationBuilder.RenameColumn(
name: "ExpirationDate",
table: "AccountRefreshTokens",
newName: "Lifetime");
}
}
}

View File

@@ -39,7 +39,6 @@ namespace WatchIt.Database.Migrations
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
@@ -48,11 +47,13 @@ namespace WatchIt.Database.Migrations
.HasMaxLength(320)
.HasColumnType("character varying(320)");
b.Property<short>("GenderId")
b.Property<short?>("GenderId")
.HasColumnType("smallint");
b.Property<bool>("IsAdmin")
.HasColumnType("boolean");
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<DateTime>("LastActive")
.ValueGeneratedOnAdd()
@@ -126,6 +127,31 @@ namespace WatchIt.Database.Migrations
b.ToTable("AccountProfilePictures");
});
modelBuilder.Entity("WatchIt.Database.Model.Account.AccountRefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<long>("AccountId")
.HasColumnType("bigint");
b.Property<DateTime>("ExpirationDate")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsExtendable")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("AccountId");
b.HasIndex("Id")
.IsUnique();
b.ToTable("AccountRefreshTokens");
});
modelBuilder.Entity("WatchIt.Database.Model.Common.Country", b =>
{
b.Property<short>("Id")
@@ -891,9 +917,7 @@ namespace WatchIt.Database.Migrations
b.HasOne("WatchIt.Database.Model.Common.Gender", "Gender")
.WithMany()
.HasForeignKey("GenderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.HasForeignKey("GenderId");
b.HasOne("WatchIt.Database.Model.Account.AccountProfilePicture", "ProfilePicture")
.WithOne("Account")
@@ -906,6 +930,17 @@ namespace WatchIt.Database.Migrations
b.Navigation("ProfilePicture");
});
modelBuilder.Entity("WatchIt.Database.Model.Account.AccountRefreshToken", b =>
{
b.HasOne("WatchIt.Database.Model.Account.Account", "Account")
.WithMany("AccountRefreshTokens")
.HasForeignKey("AccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Account");
});
modelBuilder.Entity("WatchIt.Database.Model.Media.Media", b =>
{
b.HasOne("WatchIt.Database.Model.Media.MediaMovie", null)
@@ -1188,6 +1223,8 @@ namespace WatchIt.Database.Migrations
modelBuilder.Entity("WatchIt.Database.Model.Account.Account", b =>
{
b.Navigation("AccountRefreshTokens");
b.Navigation("RatingMedia");
b.Navigation("RatingMediaSeriesEpisode");

View File

@@ -0,0 +1,26 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace WatchIt.Shared.Models.Accounts.Authenticate
{
public class AuthenticateRequest
{
#region PROPERTIES
[JsonPropertyName("username_or_email")]
public string UsernameOrEmail { get; set; }
[JsonPropertyName("password")]
public string Password { get; set; }
[JsonPropertyName("remember_me")]
public bool RememberMe { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace WatchIt.Shared.Models.Accounts.Authenticate
{
public class AuthenticateResponse
{
#region PROPERTIES
[JsonPropertyName("access_token")]
public required string AccessToken { get; init; }
[JsonPropertyName("refresh_token")]
public required string RefreshToken { get; init; }
#endregion
}
}

View File

@@ -0,0 +1,27 @@
using FluentValidation;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace WatchIt.Shared.Models.Accounts.Register
{
public class RegisterRequest
{
#region PROPERTIES
[JsonPropertyName("username")]
public string Username { get; set; }
[JsonPropertyName("email")]
public string Email { get; set; }
[JsonPropertyName("password")]
public string Password { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,44 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using WatchIt.Database.Model.Account;
namespace WatchIt.Shared.Models.Accounts.Register
{
public class RegisterResponse
{
#region PROPERTIES
[JsonPropertyName("id")]
public required long Id { get; init; }
[JsonPropertyName("username")]
public required string Username { get; init; }
[JsonPropertyName("email")]
public required string Email { get; init; }
[JsonPropertyName("creation_date")]
public required DateTime CreationDate { get; init; }
#endregion
#region CONVERTION
public static implicit operator RegisterResponse(Account account) => new RegisterResponse
{
Id = account.Id,
Username = account.Username,
Email = account.Email,
CreationDate = account.CreationDate
};
#endregion
}
}

View File

@@ -0,0 +1,107 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WatchIt.Shared.Models
{
public class RequestResult
{
#region PROPERTIES
public RequestResultStatus Status { get; }
public IEnumerable<string> ValidationMessages { get; init; }
#endregion
#region CONSTRUCTORS
internal RequestResult(RequestResultStatus status) => Status = status;
public static RequestResult Ok() => new RequestResult(RequestResultStatus.Ok);
public static RequestResult<T> Ok<T>() => new RequestResult<T>(RequestResultStatus.Ok);
public static RequestResult<T> Ok<T>(T data) => new RequestResult<T>(RequestResultStatus.Ok) { Data = data };
public static RequestResult<T> Created<T>(string location, T resource) => new RequestResult<T>(RequestResultStatus.Created) { NewResourceLocation = location, Data = resource };
public static RequestResult NoContent() => new RequestResult(RequestResultStatus.NoContent);
public static RequestResult<T> NoContent<T>() => new RequestResult<T>(RequestResultStatus.NoContent);
public static RequestResult BadRequest(params string[] validationErrors) => new RequestResult(RequestResultStatus.BadRequest) { ValidationMessages = validationErrors };
public static RequestResult<T> BadRequest<T>(params string[] validationErrors) => new RequestResult<T>(RequestResultStatus.BadRequest) { ValidationMessages = validationErrors };
public static RequestResult Unauthorized(params string[] validationErrors) => new RequestResult(RequestResultStatus.Unauthorized) { ValidationMessages = validationErrors };
public static RequestResult<T> Unauthorized<T>(params string[] validationErrors) => new RequestResult<T>(RequestResultStatus.Unauthorized) { ValidationMessages = validationErrors };
public static RequestResult Forbidden() => new RequestResult(RequestResultStatus.Forbidden);
public static RequestResult<T> Forbidden<T>() => new RequestResult<T>(RequestResultStatus.Forbidden);
public static RequestResult NotFound() => new RequestResult(RequestResultStatus.NotFound);
public static RequestResult<T> NotFound<T>() => new RequestResult<T>(RequestResultStatus.NotFound);
public static RequestResult Conflict() => new RequestResult(RequestResultStatus.Conflict);
public static RequestResult<T> Conflict<T>() => new RequestResult<T>(RequestResultStatus.Conflict);
#endregion
#region CONVERSION
public static implicit operator ActionResult(RequestResult result) => result.Status switch
{
RequestResultStatus.Ok => HandleOk(result),
RequestResultStatus.NoContent => HandleNoContent(),
RequestResultStatus.BadRequest => HandleBadRequest(result),
RequestResultStatus.Unauthorized => HandleUnauthorized(result),
RequestResultStatus.Forbidden => HandleForbidden(),
RequestResultStatus.NotFound => HandleNotFound(),
RequestResultStatus.Conflict => HandleConflict(),
};
protected static ActionResult HandleOk(RequestResult result) => new OkResult();
protected static ActionResult HandleNoContent() => new NoContentResult();
protected static ActionResult HandleBadRequest(RequestResult result) => new BadRequestObjectResult(result.ValidationMessages);
protected static ActionResult HandleUnauthorized(RequestResult result) => new UnauthorizedObjectResult(result.ValidationMessages);
protected static ActionResult HandleForbidden() => new ForbidResult();
protected static ActionResult HandleNotFound() => new NotFoundResult();
protected static ActionResult HandleConflict() => new ConflictResult();
#endregion
}
public class RequestResult<T> : RequestResult
{
#region PROPERTIES
public T? Data { get; init; }
public string? NewResourceLocation { get; init; }
#endregion
#region CONSTRUCTORS
internal RequestResult(RequestResultStatus type) : base(type) { }
#endregion
#region CONVERSION
public static implicit operator ActionResult(RequestResult<T> result) => result.Status switch
{
RequestResultStatus.Ok => HandleOk(result),
RequestResultStatus.Created => HandleCreated(result),
RequestResultStatus.NoContent => HandleNoContent(),
RequestResultStatus.BadRequest => HandleBadRequest(result),
RequestResultStatus.Unauthorized => HandleUnauthorized(result),
RequestResultStatus.Forbidden => HandleForbidden(),
RequestResultStatus.NotFound => HandleNotFound(),
RequestResultStatus.Conflict => HandleConflict(),
};
private static ActionResult HandleOk(RequestResult<T> result) => result.Data is null ? new OkResult() : new OkObjectResult(result.Data);
private static ActionResult HandleCreated(RequestResult<T> result) => new CreatedResult(result.NewResourceLocation, result.Data);
#endregion
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WatchIt.Shared.Models
{
public enum RequestResultStatus
{
Ok = 200,
Created = 201,
NoContent = 204,
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
Conflict = 409,
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\WatchIt.Database\WatchIt.Database.Model\WatchIt.Database.Model.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using WatchIt.Shared.Models.Accounts.Authenticate;
using WatchIt.Shared.Models.Accounts.Register;
using WatchIt.WebAPI.Services.Controllers;
namespace WatchIt.WebAPI.Controllers
{
[ApiController]
[Route("api/accounts")]
public class AccountsController(IAccountsControllerService accountsControllerService) : ControllerBase
{
#region METHODS
[HttpPost]
[Route("register")]
[AllowAnonymous]
[ProducesResponseType(typeof(RegisterResponse), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<ActionResult> Register([FromBody] RegisterRequest data) => await accountsControllerService.Register(data);
[HttpPost]
[Route("authenticate")]
[AllowAnonymous]
[ProducesResponseType(typeof(AuthenticateResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult> Authenticate([FromBody] AuthenticateRequest data) => await accountsControllerService.Authenticate(data);
[HttpPost]
[Route("authenticate-refresh")]
[Authorize(AuthenticationSchemes = "refresh")]
[ProducesResponseType(typeof(AuthenticateResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult> AuthenticateRefresh() => await accountsControllerService.AuthenticateRefresh();
#endregion
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,88 @@
using Microsoft.EntityFrameworkCore;
using SimpleToolkit.Extensions;
using System.Security.Cryptography;
using System.Text;
using WatchIt.Database;
using WatchIt.Database.Model.Account;
using WatchIt.Shared.Models;
using WatchIt.Shared.Models.Accounts.Authenticate;
using WatchIt.Shared.Models.Accounts.Register;
using WatchIt.WebAPI.Services.Utility.JWT;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace WatchIt.WebAPI.Services.Controllers
{
public interface IAccountsControllerService
{
Task<RequestResult<RegisterResponse>> Register(RegisterRequest data);
Task<RequestResult<AuthenticateResponse>> Authenticate(AuthenticateRequest data);
Task<RequestResult<AuthenticateResponse>> AuthenticateRefresh();
}
public class AccountsControllerService(IJWTService jwtService, DatabaseContext database) : IAccountsControllerService
{
#region PUBLIC METHODS
public async Task<RequestResult<RegisterResponse>> Register(RegisterRequest data)
{
string leftSalt = StringExtensions.CreateRandom(20);
string rightSalt = StringExtensions.CreateRandom(20);
byte[] hash = ComputeHash(data.Password, leftSalt, rightSalt);
Account account = new Account
{
Username = data.Username,
Email = data.Email,
Password = hash,
LeftSalt = leftSalt,
RightSalt = rightSalt
};
await database.Accounts.AddAsync(account);
await database.SaveChangesAsync();
return RequestResult.Created<RegisterResponse>($"accounts/{account.Id}", account);
}
public async Task<RequestResult<AuthenticateResponse>> Authenticate(AuthenticateRequest data)
{
Account? account = await database.Accounts.FirstOrDefaultAsync(x => string.Equals(x.Email, data.UsernameOrEmail) || string.Equals(x.Username, data.UsernameOrEmail));
if (account is null)
{
return RequestResult.Unauthorized<AuthenticateResponse>("User does not exists");
}
byte[] hash = ComputeHash(data.Password, account.LeftSalt, account.RightSalt);
if (!Enumerable.SequenceEqual(hash, account.Password))
{
return RequestResult.Unauthorized<AuthenticateResponse>("Incorrect password");
}
Task<string> refreshTokenTask = jwtService.CreateRefreshToken(account, true);
Task<string> accessTokenTask = jwtService.CreateAccessToken(account);
await Task.WhenAll(refreshTokenTask, accessTokenTask);
AuthenticateResponse response = new AuthenticateResponse
{
AccessToken = accessTokenTask.Result,
RefreshToken = refreshTokenTask.Result,
};
return RequestResult.Ok(response);
}
public async Task<RequestResult<AuthenticateResponse>> AuthenticateRefresh()
{
}
#endregion
#region PRIVATE METHODS
protected byte[] ComputeHash(string password, string leftSalt, string rightSalt) => SHA512.Create().ComputeHash(Encoding.UTF8.GetBytes($"{leftSalt}{password}{rightSalt}"));
#endregion
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.3" />
<PackageReference Include="SimpleToolkit.Extensions" Version="1.7.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\WatchIt.Database\WatchIt.Database\WatchIt.Database.csproj" />
<ProjectReference Include="..\..\..\WatchIt.Shared\WatchIt.Shared.Models\WatchIt.Shared.Models.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI.Services.Utility\WatchIt.WebAPI.Services.Utility.JWT\WatchIt.WebAPI.Services.Utility.JWT.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,26 @@
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WatchIt.WebAPI.Services.Utility.Configuration.Models;
namespace WatchIt.WebAPI.Services.Utility.Configuration
{
public interface IConfigurationService
{
ConfigurationData Data { get; }
}
public class ConfigurationService(IConfiguration configuration) : IConfigurationService
{
#region PROPERTIES
public ConfigurationData Data => configuration.GetSection("WebAPI").Get<ConfigurationData>()!;
#endregion
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WatchIt.WebAPI.Services.Utility.Configuration.Models
{
public class AccessToken
{
public int Lifetime { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WatchIt.WebAPI.Services.Utility.Configuration.Models
{
public class Authentication
{
public string Key { get; set; }
public string Issuer { get; set; }
public RefreshToken RefreshToken { get; set; }
public AccessToken AccessToken { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WatchIt.WebAPI.Services.Utility.Configuration.Models
{
public class ConfigurationData
{
public Authentication Authentication { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WatchIt.WebAPI.Services.Utility.Configuration.Models
{
public class RefreshToken
{
public int Lifetime { get; set; }
public int ExtendedLifetime { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,131 @@
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using WatchIt.Database;
using WatchIt.Database.Model.Account;
using WatchIt.WebAPI.Services.Utility.Configuration;
namespace WatchIt.WebAPI.Services.Utility.JWT
{
public interface IJWTService
{
Task<string> CreateAccessToken(Account account);
Task<string> CreateRefreshToken(Account account, bool extendable);
Task<string> ExtendRefreshToken(Account account, Guid id);
}
public class JWTService(IConfigurationService configurationService, DatabaseContext database) : IJWTService
{
#region PUBLIC METHODS
public async Task<string> CreateRefreshToken(Account account, bool extendable)
{
int expirationMinutes = extendable ? configurationService.Data.Authentication.RefreshToken.ExtendedLifetime : configurationService.Data.Authentication.RefreshToken.Lifetime;
DateTime expirationDate = DateTime.UtcNow.AddMinutes(expirationMinutes);
Guid id = Guid.NewGuid();
AccountRefreshToken refreshToken = new AccountRefreshToken
{
Id = id,
AccountId = account.Id,
ExpirationDate = expirationDate,
IsExtendable = extendable
};
database.AccountRefreshTokens.Add(refreshToken);
Task saveTask = database.SaveChangesAsync();
SecurityTokenDescriptor tokenDescriptor = CreateBaseSecurityTokenDescriptor(account, id, expirationDate);
tokenDescriptor.Audience = "refresh";
tokenDescriptor.Subject.AddClaim(new Claim("extend", extendable.ToString()));
string tokenString = TokenToString(tokenDescriptor);
await saveTask;
return tokenString;
}
public async Task<string> ExtendRefreshToken(Account account, Guid id)
{
AccountRefreshToken? token = account.AccountRefreshTokens.FirstOrDefault(x => x.Id == id);
if (token is null)
{
throw new TokenNotFoundException();
}
if (!token.IsExtendable)
{
throw new TokenNotExtendableException();
}
int expirationMinutes = configurationService.Data.Authentication.RefreshToken.ExtendedLifetime;
DateTime expirationDate = DateTime.UtcNow.AddMinutes(expirationMinutes);
token.ExpirationDate = expirationDate;
Task saveTask = database.SaveChangesAsync();
SecurityTokenDescriptor tokenDescriptor = CreateBaseSecurityTokenDescriptor(account, id, expirationDate);
tokenDescriptor.Audience = "refresh";
tokenDescriptor.Subject.AddClaim(new Claim("extend", bool.TrueString));
string tokenString = TokenToString(tokenDescriptor);
await saveTask;
return tokenString;
}
public async Task<string> CreateAccessToken(Account account)
{
DateTime lifetime = DateTime.Now.AddMinutes(configurationService.Data.Authentication.AccessToken.Lifetime);
Guid id = Guid.NewGuid();
SecurityTokenDescriptor tokenDescriptor = CreateBaseSecurityTokenDescriptor(account, id, lifetime);
tokenDescriptor.Audience = "access";
tokenDescriptor.Subject.AddClaim(new Claim("admin", account.IsAdmin.ToString()));
return TokenToString(tokenDescriptor);
}
#endregion
#region PRIVATE METHODS
protected SecurityTokenDescriptor CreateBaseSecurityTokenDescriptor(Account account, Guid id, DateTime expirationTime) => new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Jti, id.ToString()),
new Claim(JwtRegisteredClaimNames.Sub, account.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Email, account.Email),
new Claim(JwtRegisteredClaimNames.UniqueName, account.Username),
new Claim(JwtRegisteredClaimNames.Exp, expirationTime.ToString()),
}),
Expires = expirationTime,
Issuer = configurationService.Data.Authentication.Issuer,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configurationService.Data.Authentication.Key)), SecurityAlgorithms.HmacSha512)
};
protected string TokenToString(SecurityTokenDescriptor tokenDescriptor)
{
System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
handler.InboundClaimTypeMap.Clear();
SecurityToken token = handler.CreateToken(tokenDescriptor);
return handler.WriteToken(token);
}
#endregion
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WatchIt.WebAPI.Services.Utility.JWT
{
public class TokenNotExtendableException : Exception
{
public TokenNotExtendableException() : base() { }
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WatchIt.WebAPI.Services.Utility.JWT
{
public class TokenNotFoundException : Exception
{
public TokenNotFoundException() : base() { }
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.1.2" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.1.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\WatchIt.Database\WatchIt.Database.Model\WatchIt.Database.Model.csproj" />
<ProjectReference Include="..\..\..\..\WatchIt.Database\WatchIt.Database\WatchIt.Database.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI.Services.Utility.Configuration\WatchIt.WebAPI.Services.Utility.Configuration.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WatchIt.WebAPI.Services.Utility.User
{
public class UserService(IHttpContextAccessor accessor)
{
#region PUBLIC METHODS
#endregion
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,26 @@
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using WatchIt.Database;
using WatchIt.Shared.Models.Accounts.Authenticate;
namespace WatchIt.WebAPI.Validators.Accounts
{
public class AuthenticateRequestValidator : AbstractValidator<AuthenticateRequest>
{
#region CONSTRUCTOR
public AuthenticateRequestValidator(DatabaseContext database)
{
RuleFor(x => x.UsernameOrEmail).NotEmpty();
RuleFor(x => x.Password).NotEmpty();
}
#endregion
}
}

View File

@@ -0,0 +1,31 @@
using FluentValidation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WatchIt.Database;
using WatchIt.Shared.Models.Accounts.Register;
namespace WatchIt.WebAPI.Validators.Accounts
{
public class RegisterRequestValidator : AbstractValidator<RegisterRequest>
{
#region CONSTRUCTOR
public RegisterRequestValidator(DatabaseContext database)
{
RuleFor(x => x.Username).MinimumLength(5)
.MaximumLength(50)
.CannotBeIn(database.Accounts, x => x.Username).WithMessage("Username was already used");
RuleFor(x => x.Email).EmailAddress()
.CannotBeIn(database.Accounts, x => x.Email).WithMessage("Email was already used");
RuleFor(x => x.Password).MinimumLength(8)
.Must(x => x.Any(c => Char.IsUpper(c))).WithMessage("Password must contain at least one uppercase letter.")
.Must(x => x.Any(c => Char.IsLower(c))).WithMessage("Password must contain at least one lowercase letter.")
.Must(x => x.Any(c => Char.IsDigit(c))).WithMessage("Password must contain at least one digit.");
}
#endregion
}
}

View File

@@ -0,0 +1,18 @@
using FluentValidation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WatchIt.WebAPI.Validators
{
public static class CustomValidators
{
public static IRuleBuilderOptions<T, TProperty> CannotBeIn<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder, IEnumerable<TProperty> collection) => ruleBuilder.Must(x => !collection.Any(e => Equals(e, x)));
public static IRuleBuilderOptions<T, TProperty> CannotBeIn<T, TProperty, TCollectionType>(this IRuleBuilder<T, TProperty> ruleBuilder, IEnumerable<TCollectionType> collection, Func<TCollectionType, TProperty> propertyFunc) => ruleBuilder.Must(x => !collection.Select(propertyFunc).Any(e => Equals(e, x)));
public static IRuleBuilderOptions<T, TProperty> MustBeIn<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder, IEnumerable<TProperty> collection) => ruleBuilder.Must(x => collection.Any(e => Equals(e, x)));
public static IRuleBuilderOptions<T, TProperty> MustBeIn<T, TProperty, TCollectionType>(this IRuleBuilder<T, TProperty> ruleBuilder, IEnumerable<TCollectionType> collection, Func<TCollectionType, TProperty> propertyFunc) => ruleBuilder.Must(x => collection.Select(propertyFunc).Any(e => Equals(e, x)));
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\WatchIt.Database\WatchIt.Database\WatchIt.Database.csproj" />
<ProjectReference Include="..\..\WatchIt.Shared\WatchIt.Shared.Models\WatchIt.Shared.Models.csproj" />
</ItemGroup>
</Project>

View File

@@ -15,7 +15,27 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WatchIt.Database", "WatchIt
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WatchIt.Database.Model", "WatchIt.Database\WatchIt.Database.Model\WatchIt.Database.Model.csproj", "{46A294FF-F15F-4773-9E33-AFFC6DF2148C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WatchIt.Database.DataSeeding", "WatchIt.Database\WatchIt.Database.DataSeeding\WatchIt.Database.DataSeeding.csproj", "{EC685BDC-9C80-4D6D-94DA-F788976CD104}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WatchIt.Database.DataSeeding", "WatchIt.Database\WatchIt.Database.DataSeeding\WatchIt.Database.DataSeeding.csproj", "{EC685BDC-9C80-4D6D-94DA-F788976CD104}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WatchIt.WebAPI.Controllers", "WatchIt.WebAPI\WatchIt.WebAPI.Controllers\WatchIt.WebAPI.Controllers.csproj", "{F8EC5C47-9866-4065-AA8B-0441280BB1C9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WatchIt.Shared", "WatchIt.Shared", "{02132B7F-2055-4FA9-AA94-EDB92159AFF8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WatchIt.Shared.Models", "WatchIt.Shared\WatchIt.Shared.Models\WatchIt.Shared.Models.csproj", "{B3D90254-F594-4C15-B003-F828EEDA850A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WatchIt.WebAPI.Services", "WatchIt.WebAPI.Services", "{CD2FA54C-BAF5-41B2-B8C7-5C3EDED1B41A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WatchIt.WebAPI.Services.Controllers", "WatchIt.WebAPI\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.csproj", "{CE4669C8-E537-400A-A872-A32BD165F1AE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WatchIt.WebAPI.Validators", "WatchIt.WebAPI\WatchIt.WebAPI.Validators\WatchIt.WebAPI.Validators.csproj", "{E7DE3C54-B199-4DF8-ADA0-46FE85AA059F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WatchIt.WebAPI.Services.Utility", "WatchIt.WebAPI.Services.Utility", "{70058164-43EB-47E5-8507-23D6A41A2581}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WatchIt.WebAPI.Services.Utility.Configuration", "WatchIt.WebAPI\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Utility\WatchIt.WebAPI.Services.Utility.Configuration\WatchIt.WebAPI.Services.Utility.Configuration.csproj", "{9915A61F-A7CA-4F6F-A213-4B31BE81C3DF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WatchIt.WebAPI.Services.Utility.JWT", "WatchIt.WebAPI\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Utility\WatchIt.WebAPI.Services.Utility.JWT\WatchIt.WebAPI.Services.Utility.JWT.csproj", "{68A3C906-7470-40A1-B9C3-2F8963AA7E44}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WatchIt.WebAPI.Services.Utility.User", "WatchIt.WebAPI\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Utility\WatchIt.WebAPI.Services.Utility.User\WatchIt.WebAPI.Services.Utility.User.csproj", "{FA335A3A-BD57-472A-B3ED-43F18D9F31A6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -39,6 +59,34 @@ Global
{EC685BDC-9C80-4D6D-94DA-F788976CD104}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EC685BDC-9C80-4D6D-94DA-F788976CD104}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EC685BDC-9C80-4D6D-94DA-F788976CD104}.Release|Any CPU.Build.0 = Release|Any CPU
{F8EC5C47-9866-4065-AA8B-0441280BB1C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F8EC5C47-9866-4065-AA8B-0441280BB1C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F8EC5C47-9866-4065-AA8B-0441280BB1C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F8EC5C47-9866-4065-AA8B-0441280BB1C9}.Release|Any CPU.Build.0 = Release|Any CPU
{B3D90254-F594-4C15-B003-F828EEDA850A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B3D90254-F594-4C15-B003-F828EEDA850A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B3D90254-F594-4C15-B003-F828EEDA850A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B3D90254-F594-4C15-B003-F828EEDA850A}.Release|Any CPU.Build.0 = Release|Any CPU
{CE4669C8-E537-400A-A872-A32BD165F1AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE4669C8-E537-400A-A872-A32BD165F1AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE4669C8-E537-400A-A872-A32BD165F1AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE4669C8-E537-400A-A872-A32BD165F1AE}.Release|Any CPU.Build.0 = Release|Any CPU
{E7DE3C54-B199-4DF8-ADA0-46FE85AA059F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E7DE3C54-B199-4DF8-ADA0-46FE85AA059F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E7DE3C54-B199-4DF8-ADA0-46FE85AA059F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E7DE3C54-B199-4DF8-ADA0-46FE85AA059F}.Release|Any CPU.Build.0 = Release|Any CPU
{9915A61F-A7CA-4F6F-A213-4B31BE81C3DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9915A61F-A7CA-4F6F-A213-4B31BE81C3DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9915A61F-A7CA-4F6F-A213-4B31BE81C3DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9915A61F-A7CA-4F6F-A213-4B31BE81C3DF}.Release|Any CPU.Build.0 = Release|Any CPU
{68A3C906-7470-40A1-B9C3-2F8963AA7E44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{68A3C906-7470-40A1-B9C3-2F8963AA7E44}.Debug|Any CPU.Build.0 = Debug|Any CPU
{68A3C906-7470-40A1-B9C3-2F8963AA7E44}.Release|Any CPU.ActiveCfg = Release|Any CPU
{68A3C906-7470-40A1-B9C3-2F8963AA7E44}.Release|Any CPU.Build.0 = Release|Any CPU
{FA335A3A-BD57-472A-B3ED-43F18D9F31A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FA335A3A-BD57-472A-B3ED-43F18D9F31A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA335A3A-BD57-472A-B3ED-43F18D9F31A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA335A3A-BD57-472A-B3ED-43F18D9F31A6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -47,6 +95,15 @@ Global
{8CDAA140-05FC-4EB7-A9F5-A85032C8FD2F} = {98C91E27-2C36-4C74-A80F-9ACD7F28BC54}
{46A294FF-F15F-4773-9E33-AFFC6DF2148C} = {98C91E27-2C36-4C74-A80F-9ACD7F28BC54}
{EC685BDC-9C80-4D6D-94DA-F788976CD104} = {98C91E27-2C36-4C74-A80F-9ACD7F28BC54}
{F8EC5C47-9866-4065-AA8B-0441280BB1C9} = {76B40EBF-8054-4A15-ABE8-141E1CCA6E4E}
{B3D90254-F594-4C15-B003-F828EEDA850A} = {02132B7F-2055-4FA9-AA94-EDB92159AFF8}
{CD2FA54C-BAF5-41B2-B8C7-5C3EDED1B41A} = {76B40EBF-8054-4A15-ABE8-141E1CCA6E4E}
{CE4669C8-E537-400A-A872-A32BD165F1AE} = {CD2FA54C-BAF5-41B2-B8C7-5C3EDED1B41A}
{E7DE3C54-B199-4DF8-ADA0-46FE85AA059F} = {76B40EBF-8054-4A15-ABE8-141E1CCA6E4E}
{70058164-43EB-47E5-8507-23D6A41A2581} = {CD2FA54C-BAF5-41B2-B8C7-5C3EDED1B41A}
{9915A61F-A7CA-4F6F-A213-4B31BE81C3DF} = {70058164-43EB-47E5-8507-23D6A41A2581}
{68A3C906-7470-40A1-B9C3-2F8963AA7E44} = {70058164-43EB-47E5-8507-23D6A41A2581}
{FA335A3A-BD57-472A-B3ED-43F18D9F31A6} = {70058164-43EB-47E5-8507-23D6A41A2581}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1D253492-C786-4DD9-80B5-7DE51D4D3304}

View File

@@ -1,5 +1,16 @@
using FluentValidation;
using FluentValidation.AspNetCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Reflection;
using System.Text;
using WatchIt.Database;
using WatchIt.Shared.Models.Accounts.Register;
using WatchIt.WebAPI.Services.Controllers;
using WatchIt.WebAPI.Services.Utility.Configuration;
using WatchIt.WebAPI.Services.Utility.JWT;
using WatchIt.Website;
namespace WatchIt
@@ -23,10 +34,7 @@ namespace WatchIt
ConfigureLogging();
ConfigureDatabase();
ConfigureWebAPI();
// Add services to the container.
_builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
ConfigureWebsite();
var app = _builder.Build();
@@ -85,11 +93,86 @@ namespace WatchIt
protected static void ConfigureWebAPI()
{
_builder.Services.AddControllers();
_builder.Services.AddValidatorsFromAssembly(Assembly.Load("WatchIt.Shared.Models"));
_builder.Services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_builder.Configuration.GetValue<string>("WebAPI:Authentication:Key"))),
ValidateAudience = true,
ValidAudience = "access",
ValidIssuer = _builder.Configuration.GetValue<string>("WebAPI:Authentication:Issuer"),
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(1),
};
x.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
})
.AddJwtBearer("refresh", x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_builder.Configuration.GetValue<string>("WebAPI:Authentication:Key"))),
ValidateAudience = true,
ValidIssuer = _builder.Configuration.GetValue<string>("WebAPI:Authentication:Issuer"),
ValidAudience = "refresh",
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(1)
};
x.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
_builder.Services.AddAuthorization();
_builder.Services.AddHttpContextAccessor();
_builder.Services.AddSingleton<IConfigurationService, ConfigurationService>();
_builder.Services.AddSingleton<IJWTService, JWTService>();
_builder.Services.AddSingleton<IAccountsControllerService, AccountsControllerService>();
_builder.Services.AddFluentValidationAutoValidation();
_builder.Services.AddEndpointsApiExplorer();
_builder.Services.AddControllers();
_builder.Services.AddSwaggerGen();
}
protected static void ConfigureWebsite()
{
_builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
}
#endregion
}
}

View File

@@ -7,6 +7,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3">
<PrivateAssets>all</PrivateAssets>
@@ -26,7 +31,15 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WatchIt.Database\WatchIt.Database.DataSeeding\WatchIt.Database.DataSeeding.csproj" />
<ProjectReference Include="..\WatchIt.Database\WatchIt.Database.Model\WatchIt.Database.Model.csproj" />
<ProjectReference Include="..\WatchIt.Database\WatchIt.Database\WatchIt.Database.csproj" />
<ProjectReference Include="..\WatchIt.Shared\WatchIt.Shared.Models\WatchIt.Shared.Models.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI\WatchIt.WebAPI.Controllers\WatchIt.WebAPI.Controllers.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Utility\WatchIt.WebAPI.Services.Utility.Configuration\WatchIt.WebAPI.Services.Utility.Configuration.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Utility\WatchIt.WebAPI.Services.Utility.JWT\WatchIt.WebAPI.Services.Utility.JWT.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI\WatchIt.WebAPI.Validators\WatchIt.WebAPI.Validators.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,16 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace WatchIt.WebAPI
{
[ApiController]
[Route("api/accounts")]
public class AccountsController : ControllerBase
{
[HttpPost]
[Route("create-account")]
public async Task CreateAccount()
{
}
}
}

View File

@@ -1,12 +1,28 @@
{
"WebAPI": {
"Authentication": {
"Key": "testkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytestkeytest",
"Issuer": "WatchIt",
"RefreshToken": {
"Lifetime": 1440,
"ExtendedLifetime": 10080
},
"AccessToken": {
"Lifetime": 5
}
}
},
"Website": {
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Default": "Host=localhost;Database=watchit;Username=watchit;Password=Xdv2Etchavbuuho"
}
},
"AllowedHosts": "*"
}

View File

@@ -1,18 +0,0 @@
[
{
"Id": 1,
"Name": "Comedy"
},
{
"Id": 2,
"Name": "Thriller"
},
{
"Id": 3,
"Name": "Horror"
},
{
"Id": 4,
"Name": "Action"
}
]