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