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)