parent
7bf6f9c5ec
commit
82e59e0c86
@ -0,0 +1,16 @@
|
|||||||
|
using API.Validation;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace API;
|
||||||
|
|
||||||
|
public static class AppHttpContext
|
||||||
|
{
|
||||||
|
public static int CurrentUserId(this ControllerBase b)
|
||||||
|
{
|
||||||
|
var idClaim = b.HttpContext
|
||||||
|
.User
|
||||||
|
.Claims
|
||||||
|
.First(c => c.Type == IdentityData.IdUserClaimName);
|
||||||
|
return int.Parse(idClaim.Value);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
namespace API;
|
||||||
|
|
||||||
|
internal static class Constants
|
||||||
|
{
|
||||||
|
|
||||||
|
public const string DefaultProfilePicture =
|
||||||
|
"https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png";
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using API.Validation;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Model;
|
||||||
|
using Services;
|
||||||
|
using Services.Failures;
|
||||||
|
|
||||||
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
public class AuthenticationController(IUserService service, IConfiguration config) : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly SymmetricSecurityKey _key = new(Encoding.UTF8.GetBytes(config["JWT:Key"]!));
|
||||||
|
private static readonly TimeSpan TokenLifetime = TimeSpan.FromMinutes(50);
|
||||||
|
|
||||||
|
|
||||||
|
public record GenerateTokenRequest(
|
||||||
|
[MaxLength(256, ErrorMessage = "Email address is too wide")]
|
||||||
|
[EmailAddress]
|
||||||
|
string Email,
|
||||||
|
[MaxLength(256, ErrorMessage = "Password is too wide")]
|
||||||
|
string Password);
|
||||||
|
|
||||||
|
private record AuthenticationResponse(String Token, DateTime ExpirationDate);
|
||||||
|
|
||||||
|
[HttpPost("/auth/token")]
|
||||||
|
public async Task<IActionResult> GenerateToken([FromBody] GenerateTokenRequest req)
|
||||||
|
{
|
||||||
|
var user = await service.Authorize(req.Email, req.Password);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
return BadRequest(new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
{ "unauthorized", ["Invalid email or password"] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var (jwt, expirationDate) = GenerateJwt(user);
|
||||||
|
return Ok(new AuthenticationResponse(jwt, expirationDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public record RegisterAccountRequest(
|
||||||
|
[MaxLength(256, ErrorMessage = "name is longer than 256")]
|
||||||
|
string Username,
|
||||||
|
[MaxLength(256, ErrorMessage = "email is longer than 256")]
|
||||||
|
[EmailAddress]
|
||||||
|
string Email,
|
||||||
|
[StringLength(256, MinimumLength = 4, ErrorMessage = "password length must be between 4 and 256")]
|
||||||
|
string Password);
|
||||||
|
|
||||||
|
[HttpPost("/auth/register")]
|
||||||
|
public async Task<IActionResult> RegisterAccount([FromBody] RegisterAccountRequest req)
|
||||||
|
{
|
||||||
|
if (await service.GetUser(req.Email) != null)
|
||||||
|
{
|
||||||
|
return BadRequest(new Dictionary<string, string[]>
|
||||||
|
{
|
||||||
|
{ "email", ["The email address already exists"] }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await service.CreateUser(req.Username, req.Email, req.Password, Constants.DefaultProfilePicture,
|
||||||
|
false);
|
||||||
|
var (jwt, expirationDate) = GenerateJwt(user);
|
||||||
|
return Ok(new AuthenticationResponse(jwt, expirationDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private (string, DateTime) GenerateJwt(User user)
|
||||||
|
{
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||||
|
new(JwtRegisteredClaimNames.Sub, user.Email),
|
||||||
|
new(JwtRegisteredClaimNames.Email, user.Email),
|
||||||
|
new(IdentityData.IdUserClaimName, user.Id.ToString()),
|
||||||
|
new(IdentityData.AdminUserClaimName, user.IsAdmin.ToString())
|
||||||
|
};
|
||||||
|
|
||||||
|
var expirationDate = DateTime.UtcNow.Add(TokenLifetime);
|
||||||
|
var tokenDescriptor = new SecurityTokenDescriptor
|
||||||
|
{
|
||||||
|
Subject = new ClaimsIdentity(claims),
|
||||||
|
Expires = expirationDate,
|
||||||
|
SigningCredentials = new SigningCredentials(_key, SecurityAlgorithms.HmacSha256)
|
||||||
|
};
|
||||||
|
|
||||||
|
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||||
|
|
||||||
|
var jwt = tokenHandler.WriteToken(token);
|
||||||
|
return ("Bearer " + jwt, expirationDate);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using API.Validation;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Services;
|
||||||
|
|
||||||
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
public class TacticController(ITacticService service) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpPut("/tactic/{tacticId:int}/name")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> UpdateName(
|
||||||
|
int tacticId,
|
||||||
|
[Range(1, 50)] [Name] string name)
|
||||||
|
{
|
||||||
|
var userId = this.CurrentUserId();
|
||||||
|
if (!await service.HasAnyRights(userId, tacticId))
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await service.UpdateName(tacticId, name) ? Ok() : NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// [Authorize]
|
||||||
|
// public async Task<IActionResult> SetTacticStepContent(int tacticId, int stepId)
|
||||||
|
// {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Model;
|
||||||
|
using Services;
|
||||||
|
|
||||||
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
public class UserController(IUserService users, ITeamService teams, ITacticService tactics) : ControllerBase
|
||||||
|
{
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet("/user")]
|
||||||
|
public async Task<User> GetUser()
|
||||||
|
{
|
||||||
|
var userId = this.CurrentUserId();
|
||||||
|
return (await users.GetUser(userId))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record GetUserDataResponse(User User, Team[] Teams, Tactic[] Tactics);
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet("/user-data")]
|
||||||
|
public async Task<GetUserDataResponse> GetUserData()
|
||||||
|
{
|
||||||
|
var userId = this.CurrentUserId();
|
||||||
|
var user = await users.GetUser(userId);
|
||||||
|
var userTeams = await teams.ListTeamsOf(userId);
|
||||||
|
var userTactics = await tactics.ListTacticsOf(userId);
|
||||||
|
return new GetUserDataResponse(user!, userTeams.ToArray(), userTactics.ToArray());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace API.Validation;
|
||||||
|
|
||||||
|
internal class IdentityData
|
||||||
|
{
|
||||||
|
public const string AdminUserClaimName = "admin";
|
||||||
|
public const string AdminUserPolicyName = "Admin";
|
||||||
|
|
||||||
|
public const string IdUserClaimName = "id";
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace API.Validation;
|
||||||
|
|
||||||
|
public partial class NameAttribute : ValidationAttribute
|
||||||
|
{
|
||||||
|
protected override ValidationResult? IsValid(object? value, ValidationContext context)
|
||||||
|
{
|
||||||
|
var name = context.DisplayName;
|
||||||
|
|
||||||
|
if (value is not string str)
|
||||||
|
{
|
||||||
|
return new ValidationResult($"{name} should be a string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NameRegex().Match(str).Success)
|
||||||
|
{
|
||||||
|
return ValidationResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ValidationResult(
|
||||||
|
$"{name} should only contain numbers, letters, accents, spaces, _ and - characters.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex("[^[0-9a-zA-Zà-üÀ-Ü _-]*$]")]
|
||||||
|
private static partial Regex NameRegex();
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
using Model;
|
||||||
|
|
||||||
|
namespace AppContext.Entities;
|
||||||
|
|
||||||
|
public class TacticStepEntity
|
||||||
|
{
|
||||||
|
public required int TacticId { get; set; }
|
||||||
|
public Tactic Tactic { get; set; }
|
||||||
|
|
||||||
|
public required int ParentId { get; set; }
|
||||||
|
public TacticStepEntity Parent { get; set; }
|
||||||
|
public IEnumerable<TacticStepEntity> Children { get; set; } = new List<TacticStepEntity>();
|
||||||
|
|
||||||
|
public int StepId { get; set; }
|
||||||
|
public required string JsonContent { get; set; }
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using AppContext.Entities;
|
||||||
|
using Converters;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Model;
|
||||||
|
using Services;
|
||||||
|
|
||||||
|
namespace DbServices;
|
||||||
|
|
||||||
|
public class DbTacticService(AppContext.AppContext context) : ITacticService
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<Tactic>> ListTacticsOf(int userId)
|
||||||
|
{
|
||||||
|
return Task.FromResult(
|
||||||
|
context.Tactics
|
||||||
|
.Where(t => t.OwnerId == userId)
|
||||||
|
.AsEnumerable()
|
||||||
|
.Select(e => e.ToModel())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HasAnyRights(int userId, int tacticId)
|
||||||
|
{
|
||||||
|
var tacticEntity = await context.Tactics.FirstOrDefaultAsync(u => u.Id == tacticId);
|
||||||
|
if (tacticEntity == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return tacticEntity.OwnerId == userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateName(int tacticId, string name)
|
||||||
|
{
|
||||||
|
var entity = await context.Tactics.FirstOrDefaultAsync(t => t.Id == tacticId);
|
||||||
|
if (entity == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
entity.Name = name;
|
||||||
|
return await context.SaveChangesAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetTacticStepContent(int tacticId, int stepId, string json)
|
||||||
|
{
|
||||||
|
var entity = await context.TacticSteps
|
||||||
|
.FirstOrDefaultAsync(t => t.TacticId == tacticId && t.StepId == stepId);
|
||||||
|
if (entity == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
entity.JsonContent = json;
|
||||||
|
return await context.SaveChangesAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetTacticStepContent(int tacticId, int stepId)
|
||||||
|
{
|
||||||
|
return (await context
|
||||||
|
.TacticSteps
|
||||||
|
.FirstOrDefaultAsync(t => t.TacticId == tacticId && t.StepId == stepId)
|
||||||
|
)?.JsonContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IEnumerable<Tactic>> ListUserTactics(int userId)
|
||||||
|
{
|
||||||
|
return Task.FromResult(context
|
||||||
|
.Tactics
|
||||||
|
.Where(t => t.OwnerId == userId)
|
||||||
|
.AsEnumerable()
|
||||||
|
.Select(e => e.ToModel())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> AddTacticStep(int tacticId, int parentStepId, string initialJson)
|
||||||
|
{
|
||||||
|
var entity = new TacticStepEntity
|
||||||
|
{
|
||||||
|
JsonContent = "{components: []}",
|
||||||
|
ParentId = parentStepId,
|
||||||
|
TacticId = tacticId
|
||||||
|
};
|
||||||
|
|
||||||
|
await context.AddAsync(entity);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return entity.StepId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RemoveTacticStep(int tacticId, int stepId)
|
||||||
|
{
|
||||||
|
var toRemove = new List<int> { stepId };
|
||||||
|
|
||||||
|
while (toRemove.Count != 0)
|
||||||
|
{
|
||||||
|
var id = toRemove[0];
|
||||||
|
toRemove.RemoveAt(0);
|
||||||
|
|
||||||
|
var step = await context.TacticSteps
|
||||||
|
.Include(s => s.Children)
|
||||||
|
.FirstOrDefaultAsync(t => t.TacticId == tacticId && t.StepId == id);
|
||||||
|
|
||||||
|
if (step == null)
|
||||||
|
{
|
||||||
|
if (id == stepId)
|
||||||
|
return false;
|
||||||
|
throw new Exception(
|
||||||
|
$"step contains a children that does not exists in the database ({tacticId} / {id})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stepChildren = step.Children.Select(s => s.StepId);
|
||||||
|
toRemove.AddRange(stepChildren);
|
||||||
|
|
||||||
|
context.Remove(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync() > 0;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
using Converters;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Model;
|
||||||
|
using Services;
|
||||||
|
|
||||||
|
namespace DbServices;
|
||||||
|
|
||||||
|
public class DbTeamService(AppContext.AppContext context) : ITeamService
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<Team>> ListTeamsOf(int userId)
|
||||||
|
{
|
||||||
|
return Task.FromResult(
|
||||||
|
context.Teams
|
||||||
|
.Include(t => t.Members)
|
||||||
|
.Where(t => t.Members.Any(m => m.UserId == userId))
|
||||||
|
.AsEnumerable()
|
||||||
|
.Select(t => t.ToModel())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
namespace Model;
|
||||||
|
|
||||||
|
public record Tactic(int Id, string Name, int OwnerId, CourtType CourtType, DateTime CreationDate);
|
@ -0,0 +1,3 @@
|
|||||||
|
namespace Model;
|
||||||
|
|
||||||
|
public record Team(int Id, string Name, string Picture, string MainColor, string SecondColor);
|
@ -0,0 +1,18 @@
|
|||||||
|
using Model;
|
||||||
|
|
||||||
|
namespace Services;
|
||||||
|
|
||||||
|
public interface ITacticService
|
||||||
|
{
|
||||||
|
|
||||||
|
public Task<IEnumerable<Tactic>> ListTacticsOf(int userId);
|
||||||
|
|
||||||
|
public Task<bool> HasAnyRights(int userId, int tacticId);
|
||||||
|
|
||||||
|
public Task<bool> UpdateName(int tacticId, string name);
|
||||||
|
public Task<bool> SetTacticStepContent(int tacticId, int stepId, string json);
|
||||||
|
public Task<string?> GetTacticStepContent(int tacticId, int stepId);
|
||||||
|
public Task<IEnumerable<Tactic>> ListUserTactics(int userId);
|
||||||
|
public Task<int> AddTacticStep(int tacticId, int parentStepId, string initialJson);
|
||||||
|
public Task<bool> RemoveTacticStep(int tacticId, int stepId);
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
using Model;
|
||||||
|
|
||||||
|
namespace Services;
|
||||||
|
|
||||||
|
public interface ITeamService
|
||||||
|
{
|
||||||
|
|
||||||
|
public Task<IEnumerable<Team>> ListTeamsOf(int userId);
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in new issue