From 82e59e0c86247502b64504c845ed13f358f733ec Mon Sep 17 00:00:00 2001 From: maxime Date: Mon, 12 Feb 2024 23:05:48 +0100 Subject: [PATCH] add authentication, add basic routes for user --- .gitignore | 2 + API/API.csproj | 24 +++- API/AppHttpContext.cs | 16 +++ API/Constants.cs | 10 ++ API/Controllers/AccountsController.cs | 11 +- API/Controllers/AuthenticationController.cs | 99 +++++++++++++++ API/Controllers/TacticController.cs | 32 +++++ API/Controllers/UserController.cs | 31 +++++ API/Program.cs | 60 ++++++++- API/Properties/launchSettings.json | 2 +- API/Validation/IdentityData.cs | 9 ++ API/Validation/NameAttribute.cs | 28 +++++ API/appsettings.json | 6 +- AppContext/AppContext.cs | 17 ++- AppContext/AppContext.csproj | 1 + AppContext/Entities/MemberEntity.cs | 1 + AppContext/Entities/TacticEntity.cs | 3 + AppContext/Entities/TacticStepEntity.cs | 16 +++ Converters/ModelToEntities.cs | 13 +- DbServices/DbTacticService.cs | 115 ++++++++++++++++++ DbServices/DbTeamService.cs | 20 +++ DbServices/DbUserService.cs | 15 ++- Model/Tactic.cs | 3 + Model/Team.cs | 3 + Services/Failures/Failure.cs | 5 + Services/ITacticService.cs | 18 +++ Services/ITeamService.cs | 10 ++ Services/UserService.cs | 5 +- ...igner.cs => 20240212203956_m1.Designer.cs} | 19 ++- ...40212084944_m1.cs => 20240212203956_m1.cs} | 16 +-- .../Migrations/StubAppContextModelSnapshot.cs | 17 +++ StubContext/StubAppContext.cs | 2 +- StubContext/StubContext.csproj | 1 + 33 files changed, 599 insertions(+), 31 deletions(-) create mode 100644 API/AppHttpContext.cs create mode 100644 API/Constants.cs create mode 100644 API/Controllers/AuthenticationController.cs create mode 100644 API/Controllers/TacticController.cs create mode 100644 API/Controllers/UserController.cs create mode 100644 API/Validation/IdentityData.cs create mode 100644 API/Validation/NameAttribute.cs create mode 100644 AppContext/Entities/TacticStepEntity.cs create mode 100644 DbServices/DbTacticService.cs create mode 100644 DbServices/DbTeamService.cs create mode 100644 Model/Tactic.cs create mode 100644 Model/Team.cs create mode 100644 Services/ITacticService.cs create mode 100644 Services/ITeamService.cs rename StubContext/Migrations/{20240212084944_m1.Designer.cs => 20240212203956_m1.Designer.cs} (88%) rename StubContext/Migrations/{20240212084944_m1.cs => 20240212203956_m1.cs} (79%) diff --git a/.gitignore b/.gitignore index 84d3341..c65e4ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .idea .vs +*.db* + # Build results [Dd]ebug/ diff --git a/API/API.csproj b/API/API.csproj index dbebf35..dbdf275 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -6,11 +6,19 @@ enable true Linux + $(MSBuildProjectDirectory) - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + @@ -20,7 +28,21 @@ + + + + + + + ..\..\..\..\.nuget\packages\microsoft.aspnetcore.authentication.jwtbearer\8.0.0\lib\net8.0\Microsoft.AspNetCore.Authentication.JwtBearer.dll + + + ..\..\..\..\.nuget\packages\microsoft.identitymodel.tokens\7.0.3\lib\net8.0\Microsoft.IdentityModel.Tokens.dll + + + ..\..\..\..\.nuget\packages\system.identitymodel.tokens.jwt\7.0.3\lib\net8.0\System.IdentityModel.Tokens.Jwt.dll + diff --git a/API/AppHttpContext.cs b/API/AppHttpContext.cs new file mode 100644 index 0000000..ff1e504 --- /dev/null +++ b/API/AppHttpContext.cs @@ -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); + } +} \ No newline at end of file diff --git a/API/Constants.cs b/API/Constants.cs new file mode 100644 index 0000000..1c4aa3d --- /dev/null +++ b/API/Constants.cs @@ -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"; + + +} \ No newline at end of file diff --git a/API/Controllers/AccountsController.cs b/API/Controllers/AccountsController.cs index ae97ad3..e8f30af 100644 --- a/API/Controllers/AccountsController.cs +++ b/API/Controllers/AccountsController.cs @@ -5,11 +5,12 @@ using Services; namespace API.Controllers; -public class AccountsController(UserService service) : ControllerBase +[ApiController] +public class AccountsController(IUserService service) : ControllerBase { - private static string _defaultProfilePicture = + private const string DefaultProfilePicture = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png"; - + [HttpGet("/admin/list-users")] public async Task> ListUsers( [Range(0, int.MaxValue, ErrorMessage = "Only positive number allowed")] @@ -51,7 +52,7 @@ public class AccountsController(UserService service) : ControllerBase bool isAdmin = false ) { - return service.CreateUser(username, email, password, _defaultProfilePicture, isAdmin); + return service.CreateUser(username, email, password, DefaultProfilePicture, isAdmin); } [HttpDelete("/admin/user")] @@ -72,7 +73,7 @@ public class AccountsController(UserService service) : ControllerBase { try { - await service.UpdateUser(new User(id, username, email, _defaultProfilePicture, isAdmin)); + await service.UpdateUser(new User(id, username, email, DefaultProfilePicture, isAdmin)); return Ok(); } catch (ServiceException e) diff --git a/API/Controllers/AuthenticationController.cs b/API/Controllers/AuthenticationController.cs new file mode 100644 index 0000000..2af78e1 --- /dev/null +++ b/API/Controllers/AuthenticationController.cs @@ -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 GenerateToken([FromBody] GenerateTokenRequest req) + { + var user = await service.Authorize(req.Email, req.Password); + + if (user == null) + return BadRequest(new Dictionary + { + { "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 RegisterAccount([FromBody] RegisterAccountRequest req) + { + if (await service.GetUser(req.Email) != null) + { + return BadRequest(new Dictionary + { + { "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 + { + 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); + } +} \ No newline at end of file diff --git a/API/Controllers/TacticController.cs b/API/Controllers/TacticController.cs new file mode 100644 index 0000000..3849adb --- /dev/null +++ b/API/Controllers/TacticController.cs @@ -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 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 SetTacticStepContent(int tacticId, int stepId) + // { + // + // } +} \ No newline at end of file diff --git a/API/Controllers/UserController.cs b/API/Controllers/UserController.cs new file mode 100644 index 0000000..ece3bef --- /dev/null +++ b/API/Controllers/UserController.cs @@ -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 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 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()); + } +} \ No newline at end of file diff --git a/API/Program.cs b/API/Program.cs index f547eae..fab8620 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,10 +1,58 @@ +using System.Net.Mime; +using API.Validation; +using DbServices; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using Services; +using StubContext; +using System.Text; + var builder = WebApplication.CreateBuilder(args); +var config = builder.Configuration; // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -builder.Services.AddControllers(); +builder.Services.AddControllers() + .ConfigureApiBehaviorOptions(options => options.InvalidModelStateResponseFactory = context => + new BadRequestObjectResult(context.ModelState) + { + ContentTypes = { MediaTypeNames.Application.Json } + }); + +builder.Services.AddCors(options => + options.AddPolicy("cors", policy => + policy + .AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod() + )); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer( + x => x.TokenValidationParameters = new TokenValidationParameters + { + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["JWT:Key"]!)), + ValidateLifetime = true, + ValidateIssuer = false, + ValidateAudience = false, + ValidateIssuerSigningKey = true + } + ); + +builder.Services.AddAuthorization(options => + options.AddPolicy(IdentityData.AdminUserPolicyName, p => p.RequireClaim(IdentityData.AdminUserClaimName))); + + +var appContext = new StubAppContext(); +appContext.Database.EnsureCreated(); + +builder.Services.AddScoped(_ => new DbUserService(appContext)); +builder.Services.AddScoped(_ => new DbTeamService(appContext)); +builder.Services.AddScoped(_ => new DbTacticService(appContext)); + var app = builder.Build(); @@ -15,7 +63,13 @@ if (app.Environment.IsDevelopment()) app.UseSwaggerUI(); } -app.MapControllers(); app.UseHttpsRedirection(); +app.UseCors("cors"); + +app.UseRouting(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); + -app.Run(); +app.Run(); \ No newline at end of file diff --git a/API/Properties/launchSettings.json b/API/Properties/launchSettings.json index 7653faf..27799cf 100644 --- a/API/Properties/launchSettings.json +++ b/API/Properties/launchSettings.json @@ -12,7 +12,7 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", "applicationUrl": "http://localhost:5254", "environmentVariables": { diff --git a/API/Validation/IdentityData.cs b/API/Validation/IdentityData.cs new file mode 100644 index 0000000..9af9cd0 --- /dev/null +++ b/API/Validation/IdentityData.cs @@ -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"; +} \ No newline at end of file diff --git a/API/Validation/NameAttribute.cs b/API/Validation/NameAttribute.cs new file mode 100644 index 0000000..62f6eb7 --- /dev/null +++ b/API/Validation/NameAttribute.cs @@ -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(); +} \ No newline at end of file diff --git a/API/appsettings.json b/API/appsettings.json index 10f68b8..43e8833 100644 --- a/API/appsettings.json +++ b/API/appsettings.json @@ -1,8 +1,12 @@ { + "JWT": { + "Key": "Remember to use a secret key on prod" + }, "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "Microsoft.AspNetCore.Authentication": "Trace" } }, "AllowedHosts": "*" diff --git a/AppContext/AppContext.cs b/AppContext/AppContext.cs index 68a8c7c..41b08be 100644 --- a/AppContext/AppContext.cs +++ b/AppContext/AppContext.cs @@ -7,14 +7,15 @@ namespace AppContext; public class AppContext(DbContextOptions options) : DbContext(options) { - public DbSet Users { get; } - public DbSet Tactics { get; } - public DbSet Teams { get; } - public DbSet Members { get; } + public DbSet Users { get; init; } + public DbSet Tactics { get; init; } + public DbSet Teams { get; init; } + public DbSet Members { get; init; } + public DbSet TacticSteps { get; set; } public AppContext() : this( new DbContextOptionsBuilder() - .UseSqlite("Data Source=database.db") + .UseSqlite("DataSource=database.db") .Options ) { @@ -29,6 +30,12 @@ public class AppContext(DbContextOptions options) : DbContext(option v => HashString(v), v => v ); + + builder.Entity() + .HasKey("UserId", "TeamId"); + + builder.Entity() + .HasKey("TacticId", "StepId"); } private static string HashString(string str) diff --git a/AppContext/AppContext.csproj b/AppContext/AppContext.csproj index 22d9aba..3ed2224 100644 --- a/AppContext/AppContext.csproj +++ b/AppContext/AppContext.csproj @@ -8,6 +8,7 @@ + diff --git a/AppContext/Entities/MemberEntity.cs b/AppContext/Entities/MemberEntity.cs index 784be6e..7ba1c20 100644 --- a/AppContext/Entities/MemberEntity.cs +++ b/AppContext/Entities/MemberEntity.cs @@ -2,6 +2,7 @@ namespace AppContext.Entities; public class MemberEntity { + public int TeamId { get; set; } public TeamEntity? Team { get; set; } diff --git a/AppContext/Entities/TacticEntity.cs b/AppContext/Entities/TacticEntity.cs index cb042e2..899fd70 100644 --- a/AppContext/Entities/TacticEntity.cs +++ b/AppContext/Entities/TacticEntity.cs @@ -13,4 +13,7 @@ public class TacticEntity public CourtType Type { get; set; } public required string JsonContent { get; set; } + + public required int RootStepId { get; set; } + public TacticStepEntity RootStep { get; set; } } \ No newline at end of file diff --git a/AppContext/Entities/TacticStepEntity.cs b/AppContext/Entities/TacticStepEntity.cs new file mode 100644 index 0000000..d6f9a5f --- /dev/null +++ b/AppContext/Entities/TacticStepEntity.cs @@ -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 Children { get; set; } = new List(); + + public int StepId { get; set; } + public required string JsonContent { get; set; } +} \ No newline at end of file diff --git a/Converters/ModelToEntities.cs b/Converters/ModelToEntities.cs index bbf17b2..b64acb1 100644 --- a/Converters/ModelToEntities.cs +++ b/Converters/ModelToEntities.cs @@ -10,8 +10,13 @@ public static class EntitiesToModels return new User(entity.Id, entity.Name, entity.Email, entity.ProfilePicture, entity.IsAdmin); } - // public static Team ToModel(this TeamEntity entity) - // { - // - // } + public static Tactic ToModel(this TacticEntity entity) + { + return new Tactic(entity.Id, entity.Name, entity.OwnerId, entity.Type, entity.CreationDate); + } + + public static Team ToModel(this TeamEntity entity) + { + return new Team(entity.Id, entity.Name, entity.Picture, entity.MainColor, entity.SecondColor); + } } \ No newline at end of file diff --git a/DbServices/DbTacticService.cs b/DbServices/DbTacticService.cs new file mode 100644 index 0000000..2a91d34 --- /dev/null +++ b/DbServices/DbTacticService.cs @@ -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> ListTacticsOf(int userId) + { + return Task.FromResult( + context.Tactics + .Where(t => t.OwnerId == userId) + .AsEnumerable() + .Select(e => e.ToModel()) + ); + } + + public async Task 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 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 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 GetTacticStepContent(int tacticId, int stepId) + { + return (await context + .TacticSteps + .FirstOrDefaultAsync(t => t.TacticId == tacticId && t.StepId == stepId) + )?.JsonContent; + } + + public Task> ListUserTactics(int userId) + { + return Task.FromResult(context + .Tactics + .Where(t => t.OwnerId == userId) + .AsEnumerable() + .Select(e => e.ToModel()) + ); + } + + public async Task 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 RemoveTacticStep(int tacticId, int stepId) + { + var toRemove = new List { 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; + } +} \ No newline at end of file diff --git a/DbServices/DbTeamService.cs b/DbServices/DbTeamService.cs new file mode 100644 index 0000000..af22160 --- /dev/null +++ b/DbServices/DbTeamService.cs @@ -0,0 +1,20 @@ +using Converters; +using Microsoft.EntityFrameworkCore; +using Model; +using Services; + +namespace DbServices; + +public class DbTeamService(AppContext.AppContext context) : ITeamService +{ + public Task> 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()) + ); + } +} \ No newline at end of file diff --git a/DbServices/DbUserService.cs b/DbServices/DbUserService.cs index c80544c..1471391 100644 --- a/DbServices/DbUserService.cs +++ b/DbServices/DbUserService.cs @@ -8,7 +8,7 @@ using Services.Failures; namespace DbServices; -public class DbUserService(AppContext.AppContext context) : UserService +public class DbUserService(AppContext.AppContext context) : IUserService { public Task> ListUsers(string nameNeedle) { @@ -34,6 +34,11 @@ public class DbUserService(AppContext.AppContext context) : UserService return (await context.Users.FirstOrDefaultAsync(e => e.Id == id))?.ToModel(); } + public async Task GetUser(string email) + { + return (await context.Users.FirstOrDefaultAsync(e => e.Email == email))?.ToModel(); + } + public async Task CreateUser(string username, string email, string password, string profilePicture, bool isAdmin) { @@ -69,4 +74,12 @@ public class DbUserService(AppContext.AppContext context) : UserService entity.Id = user.Id; await context.SaveChangesAsync(); } + + public async Task Authorize(string email, string password) + { + return (await context + .Users + .FirstOrDefaultAsync(u => u.Email == email)) + ?.ToModel(); + } } \ No newline at end of file diff --git a/Model/Tactic.cs b/Model/Tactic.cs new file mode 100644 index 0000000..49884c7 --- /dev/null +++ b/Model/Tactic.cs @@ -0,0 +1,3 @@ +namespace Model; + +public record Tactic(int Id, string Name, int OwnerId, CourtType CourtType, DateTime CreationDate); \ No newline at end of file diff --git a/Model/Team.cs b/Model/Team.cs new file mode 100644 index 0000000..216afec --- /dev/null +++ b/Model/Team.cs @@ -0,0 +1,3 @@ +namespace Model; + +public record Team(int Id, string Name, string Picture, string MainColor, string SecondColor); \ No newline at end of file diff --git a/Services/Failures/Failure.cs b/Services/Failures/Failure.cs index 0d3a150..7d1c427 100644 --- a/Services/Failures/Failure.cs +++ b/Services/Failures/Failure.cs @@ -6,4 +6,9 @@ public record Failure(string Name, string Message) { return new("not found", message); } + + public static Failure Forbidden(string message) + { + return new("forbidden", message); + } } \ No newline at end of file diff --git a/Services/ITacticService.cs b/Services/ITacticService.cs new file mode 100644 index 0000000..558f120 --- /dev/null +++ b/Services/ITacticService.cs @@ -0,0 +1,18 @@ +using Model; + +namespace Services; + +public interface ITacticService +{ + + public Task> ListTacticsOf(int userId); + + public Task HasAnyRights(int userId, int tacticId); + + public Task UpdateName(int tacticId, string name); + public Task SetTacticStepContent(int tacticId, int stepId, string json); + public Task GetTacticStepContent(int tacticId, int stepId); + public Task> ListUserTactics(int userId); + public Task AddTacticStep(int tacticId, int parentStepId, string initialJson); + public Task RemoveTacticStep(int tacticId, int stepId); +} \ No newline at end of file diff --git a/Services/ITeamService.cs b/Services/ITeamService.cs new file mode 100644 index 0000000..81e31bc --- /dev/null +++ b/Services/ITeamService.cs @@ -0,0 +1,10 @@ +using Model; + +namespace Services; + +public interface ITeamService +{ + + public Task> ListTeamsOf(int userId); + +} \ No newline at end of file diff --git a/Services/UserService.cs b/Services/UserService.cs index 3e30634..836d6a8 100644 --- a/Services/UserService.cs +++ b/Services/UserService.cs @@ -2,18 +2,21 @@ namespace Services; -public interface UserService +public interface IUserService { Task> ListUsers(string nameNeedle); Task> ListUsers(); Task GetUser(int id); + Task GetUser(string email); Task CreateUser(string username, string email, string password, string profilePicture, bool isAdmin); Task RemoveUsers(params int[] identifiers); Task UpdateUser(User user); + + public Task Authorize(string email, string password); } \ No newline at end of file diff --git a/StubContext/Migrations/20240212084944_m1.Designer.cs b/StubContext/Migrations/20240212203956_m1.Designer.cs similarity index 88% rename from StubContext/Migrations/20240212084944_m1.Designer.cs rename to StubContext/Migrations/20240212203956_m1.Designer.cs index bb22526..e390b1c 100644 --- a/StubContext/Migrations/20240212084944_m1.Designer.cs +++ b/StubContext/Migrations/20240212203956_m1.Designer.cs @@ -11,7 +11,7 @@ using StubContext; namespace StubContext.Migrations { [DbContext(typeof(StubAppContext))] - [Migration("20240212084944_m1")] + [Migration("20240212203956_m1")] partial class m1 { /// @@ -106,10 +106,17 @@ namespace StubContext.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + b.Property("Name") .IsRequired() .HasColumnType("TEXT"); + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("ProfilePicture") .IsRequired() .HasColumnType("TEXT"); @@ -123,35 +130,45 @@ namespace StubContext.Migrations { Id = 1, Email = "maxime@mail.com", + IsAdmin = true, Name = "maxime", + Password = "Abq2gmly3eYK29oU+krGTUfRH2MAka//xH4nqbZV1Ak=", ProfilePicture = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, new { Id = 2, Email = "mael@mail.com", + IsAdmin = true, Name = "mael", + Password = "b4IE5XGndyILJn/ZgMdX9YH+rcux1Rvp9X4adjzXzto=", ProfilePicture = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, new { Id = 3, Email = "yanis@mail.com", + IsAdmin = true, Name = "yanis", + Password = "XZOJugDxViwhvojERZCWfwocYdwICNTyl8VwNZjdVdM=", ProfilePicture = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, new { Id = 4, Email = "simon@mail.com", + IsAdmin = true, Name = "simon", + Password = "FzsWErXcFnZrRwgY6j18tXdFzq+Jm+jmYKeOTFVMy68=", ProfilePicture = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, new { Id = 5, Email = "vivien@mail.com", + IsAdmin = true, Name = "vivien", + Password = "6oIi8obqxLNGf1UdE6dFVbffc/+z93AoZiZieibD5vM=", ProfilePicture = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }); }); diff --git a/StubContext/Migrations/20240212084944_m1.cs b/StubContext/Migrations/20240212203956_m1.cs similarity index 79% rename from StubContext/Migrations/20240212084944_m1.cs rename to StubContext/Migrations/20240212203956_m1.cs index 45f9733..b7684a1 100644 --- a/StubContext/Migrations/20240212084944_m1.cs +++ b/StubContext/Migrations/20240212203956_m1.cs @@ -35,9 +35,11 @@ namespace StubContext.Migrations { Id = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), + Password = table.Column(type: "TEXT", nullable: false), Name = table.Column(type: "TEXT", nullable: false), Email = table.Column(type: "TEXT", nullable: false), - ProfilePicture = table.Column(type: "TEXT", nullable: false) + ProfilePicture = table.Column(type: "TEXT", nullable: false), + IsAdmin = table.Column(type: "INTEGER", nullable: false) }, constraints: table => { @@ -94,14 +96,14 @@ namespace StubContext.Migrations migrationBuilder.InsertData( table: "Users", - columns: new[] { "Id", "Email", "Name", "ProfilePicture" }, + columns: new[] { "Id", "Email", "IsAdmin", "Name", "Password", "ProfilePicture" }, values: new object[,] { - { 1, "maxime@mail.com", "maxime", "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, - { 2, "mael@mail.com", "mael", "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, - { 3, "yanis@mail.com", "yanis", "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, - { 4, "simon@mail.com", "simon", "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, - { 5, "vivien@mail.com", "vivien", "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" } + { 1, "maxime@mail.com", true, "maxime", "lhrwN9GTh2+T/KWpOnGJTI5rt8mZVrljOtDRuYv9Xck=", "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, + { 2, "mael@mail.com", true, "mael", "m+9+DO+fDO5BH33h6rpEvHwVlFTt4Xb0Sa3/qGmDymI=", "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, + { 3, "yanis@mail.com", true, "yanis", "I63Z72IN7LSzp1nG18wX2yAN2kM16WOxMIIMI3o5Nno=", "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, + { 4, "simon@mail.com", true, "simon", "SGbhGgYSXuQpyvdKOlTrUxJcrV3iBZGt5NFZiWtxKWw=", "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, + { 5, "vivien@mail.com", true, "vivien", "v5dtqo9qg96d7ajz+DQlvVZ1Mbeu9RstFXtuhX5SGfo=", "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" } }); migrationBuilder.CreateIndex( diff --git a/StubContext/Migrations/StubAppContextModelSnapshot.cs b/StubContext/Migrations/StubAppContextModelSnapshot.cs index 2863d77..92ceed5 100644 --- a/StubContext/Migrations/StubAppContextModelSnapshot.cs +++ b/StubContext/Migrations/StubAppContextModelSnapshot.cs @@ -103,10 +103,17 @@ namespace StubContext.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + b.Property("Name") .IsRequired() .HasColumnType("TEXT"); + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("ProfilePicture") .IsRequired() .HasColumnType("TEXT"); @@ -120,35 +127,45 @@ namespace StubContext.Migrations { Id = 1, Email = "maxime@mail.com", + IsAdmin = true, Name = "maxime", + Password = "cRJvoN/47sgLBqqriPmmThe0HRkZ0IvXw6qomIcnZ7M=", ProfilePicture = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, new { Id = 2, Email = "mael@mail.com", + IsAdmin = true, Name = "mael", + Password = "opi1I908QEQ3POUbgBjHd/998dWJmvVTmRuDcxlv3do=", ProfilePicture = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, new { Id = 3, Email = "yanis@mail.com", + IsAdmin = true, Name = "yanis", + Password = "7qmnCq6Kto7dEMhH7chzpqetNkbAMFhxLMxMKo599ws=", ProfilePicture = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, new { Id = 4, Email = "simon@mail.com", + IsAdmin = true, Name = "simon", + Password = "Hzx7qgkwFMtVZsMp+DK6+ui8AGhbxSeCMC2bOdvLaQA=", ProfilePicture = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }, new { Id = 5, Email = "vivien@mail.com", + IsAdmin = true, Name = "vivien", + Password = "Cjntk37NpF7fct5OfJftgzeZi+ki4Y8aYHsKKm9RmBA=", ProfilePicture = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png" }); }); diff --git a/StubContext/StubAppContext.cs b/StubContext/StubAppContext.cs index bb00f2c..39a944e 100644 --- a/StubContext/StubAppContext.cs +++ b/StubContext/StubAppContext.cs @@ -9,7 +9,7 @@ public class StubAppContext(DbContextOptions options) : AppContext(o { public StubAppContext() : this( new DbContextOptionsBuilder() - .UseSqlite("Data Source=database.db") + .UseSqlite("DataSource=database.db") .Options ) { diff --git a/StubContext/StubContext.csproj b/StubContext/StubContext.csproj index 51c5796..4a83198 100644 --- a/StubContext/StubContext.csproj +++ b/StubContext/StubContext.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + $(MSBuildProjectDirectory)