Compare commits

...

14 Commits

Author SHA1 Message Date
maxime ff1219c436 add plain user data in teams/members request
continuous-integration/drone/push Build is passing Details
1 year ago
maxime cfddcb9f81 add getTeam route
continuous-integration/drone/push Build is passing Details
1 year ago
maxime e81b7dd24d update colors regex
continuous-integration/drone/push Build is passing Details
1 year ago
maxime 107c9f5282 add ChangeUserInformation UT
continuous-integration/drone/push Build is passing Details
1 year ago
maxime ecd9028d04 fix CI
continuous-integration/drone/push Build is failing Details
1 year ago
maxime 29fc5af697 fix sonnar coverage
continuous-integration/drone/push Build is passing Details
1 year ago
maxime eb1053ca52 add root step id in the response of a create tactic request
continuous-integration/drone/push Build is passing Details
1 year ago
maxime.batista b3ba44f127 fix ChangeUserInformation
continuous-integration/drone/push Build is passing Details
1 year ago
maxime 6ae765733a add remove tactic route
continuous-integration/drone/push Build is passing Details
1 year ago
maxime aab1eb74a2 add profile edition route and a keep alive route
continuous-integration/drone/push Build is passing Details
1 year ago
maxime 7714126252 add canEdit route
continuous-integration/drone/push Build is passing Details
1 year ago
maxime 2d6a7be4f2 replace dates by epoch time millis
continuous-integration/drone/push Build is passing Details
1 year ago
maxime 89880432c1 fix concurrent PGSQL error
continuous-integration/drone/push Build is passing Details
1 year ago
Vivien DUFOUR ea827253e4 Merge pull request 'shareTactic' (#2) from shareTactic into master
continuous-integration/drone/push Build is passing Details
1 year ago

@ -1,5 +1,7 @@
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
namespace API.Auth; namespace API.Auth;
@ -25,5 +27,4 @@ public static class Authentication
return ("Bearer " + jwt, expirationDate); return ("Bearer " + jwt, expirationDate);
} }
} }

@ -120,7 +120,7 @@ public class UsersAdminController(IUserService service, ILogger<UsersAdminContro
} }
catch (ServiceException e) catch (ServiceException e)
{ {
return BadRequest(e.Failures); return BadRequest(e.FailuresMessages());
} }
} }
} }

@ -1,10 +1,10 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography;
using System.Text; using System.Text;
using API.Auth; using API.Auth;
using API.Validation; using API.Validation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Model; using Model;
@ -16,7 +16,15 @@ namespace API.Controllers;
public class AuthenticationController(IUserService service, IConfiguration config) : ControllerBase public class AuthenticationController(IUserService service, IConfiguration config) : ControllerBase
{ {
private readonly SymmetricSecurityKey _key = new(Encoding.UTF8.GetBytes(config["JWT:Key"]!)); private readonly SymmetricSecurityKey _key = new(Encoding.UTF8.GetBytes(config["JWT:Key"]!));
[HttpGet("/auth/keep-alive")]
[Authorize]
public void KeepAlive()
{
}
public record GenerateTokenRequest( public record GenerateTokenRequest(
[MaxLength(256, ErrorMessage = "Email address is too wide")] [MaxLength(256, ErrorMessage = "Email address is too wide")]
[EmailAddress] [EmailAddress]
@ -25,7 +33,7 @@ public class AuthenticationController(IUserService service, IConfiguration confi
string Password string Password
); );
private record AuthenticationResponse(String Token, DateTime ExpirationDate); private record AuthenticationResponse(String Token, long ExpirationDate);
[HttpPost("/auth/token")] [HttpPost("/auth/token")]
public async Task<IActionResult> GenerateToken([FromBody] GenerateTokenRequest req) public async Task<IActionResult> GenerateToken([FromBody] GenerateTokenRequest req)
@ -39,12 +47,13 @@ public class AuthenticationController(IUserService service, IConfiguration confi
}); });
var (jwt, expirationDate) = GenerateJwt(user); var (jwt, expirationDate) = GenerateJwt(user);
return Ok(new AuthenticationResponse(jwt, expirationDate)); return Ok(new AuthenticationResponse(jwt, expirationDate.ToFileTimeUtc()));
} }
public record RegisterAccountRequest( public record RegisterAccountRequest(
[MaxLength(256, ErrorMessage = "name is longer than 256")] [StringLength(256, MinimumLength = 4, ErrorMessage = "password length must be between 4 and 256")]
[Name]
string Username, string Username,
[MaxLength(256, ErrorMessage = "email is longer than 256")] [MaxLength(256, ErrorMessage = "email is longer than 256")]
[EmailAddress] [EmailAddress]
@ -62,8 +71,7 @@ public class AuthenticationController(IUserService service, IConfiguration confi
{ "email", ["The email address already exists"] } { "email", ["The email address already exists"] }
}); });
} }
var user = await service.CreateUser( var user = await service.CreateUser(
req.Username, req.Username,
req.Email, req.Email,
@ -73,7 +81,7 @@ public class AuthenticationController(IUserService service, IConfiguration confi
); );
var (jwt, expirationDate) = GenerateJwt(user); var (jwt, expirationDate) = GenerateJwt(user);
return Ok(new AuthenticationResponse(jwt, expirationDate)); return Ok(new AuthenticationResponse(jwt, expirationDate.ToFileTimeUtc()));
} }
@ -90,4 +98,6 @@ public class AuthenticationController(IUserService service, IConfiguration confi
return Authentication.GenerateJwt(_key, claims); return Authentication.GenerateJwt(_key, claims);
} }
} }

@ -26,7 +26,7 @@ public class TacticController(ITacticService service, IContextAccessor accessor)
[FromBody] UpdateNameRequest req) [FromBody] UpdateNameRequest req)
{ {
var userId = accessor.CurrentUserId(HttpContext); var userId = accessor.CurrentUserId(HttpContext);
if (!await service.HasAnyRights(userId, tacticId)) if (!await service.IsOwnerOf(userId, tacticId))
{ {
return Unauthorized(); return Unauthorized();
} }
@ -41,7 +41,7 @@ public class TacticController(ITacticService service, IContextAccessor accessor)
public async Task<IActionResult> GetTacticInfo(int tacticId) public async Task<IActionResult> GetTacticInfo(int tacticId)
{ {
var userId = accessor.CurrentUserId(HttpContext); var userId = accessor.CurrentUserId(HttpContext);
if (!await service.HasAnyRights(userId, tacticId)) if (!await service.IsOwnerOf(userId, tacticId))
{ {
return Unauthorized(); return Unauthorized();
} }
@ -57,7 +57,7 @@ public class TacticController(ITacticService service, IContextAccessor accessor)
public async Task<IActionResult> GetTacticStepsRoot(int tacticId) public async Task<IActionResult> GetTacticStepsRoot(int tacticId)
{ {
var userId = accessor.CurrentUserId(HttpContext); var userId = accessor.CurrentUserId(HttpContext);
if (!await service.HasAnyRights(userId, tacticId)) if (!await service.IsOwnerOf(userId, tacticId))
{ {
return Unauthorized(); return Unauthorized();
} }
@ -74,7 +74,7 @@ public class TacticController(ITacticService service, IContextAccessor accessor)
[AllowedValues("PLAIN", "HALF")] string CourtType [AllowedValues("PLAIN", "HALF")] string CourtType
); );
public record CreateNewResponse(int Id); public record CreateNewResponse(int Id, int RootStepId);
[HttpPost("/tactics")] [HttpPost("/tactics")]
[Authorize] [Authorize]
@ -88,8 +88,8 @@ public class TacticController(ITacticService service, IContextAccessor accessor)
throw new ArgumentOutOfRangeException("for req.CourtType"); throw new ArgumentOutOfRangeException("for req.CourtType");
} }
var id = await service.AddTactic(userId, req.Name, courtType); var (id, rootId) = await service.AddTactic(userId, req.Name, courtType);
return new CreateNewResponse(id); return new CreateNewResponse(id, rootId);
} }
public record AddStepRequest(int ParentId, object Content); public record AddStepRequest(int ParentId, object Content);
@ -110,7 +110,7 @@ public class TacticController(ITacticService service, IContextAccessor accessor)
{ {
var userId = accessor.CurrentUserId(HttpContext); var userId = accessor.CurrentUserId(HttpContext);
if (!await service.HasAnyRights(userId, tacticId)) if (!await service.IsOwnerOf(userId, tacticId))
{ {
return Unauthorized(); return Unauthorized();
} }
@ -125,7 +125,7 @@ public class TacticController(ITacticService service, IContextAccessor accessor)
{ {
var userId = accessor.CurrentUserId(HttpContext); var userId = accessor.CurrentUserId(HttpContext);
if (!await service.HasAnyRights(userId, tacticId)) if (!await service.IsOwnerOf(userId, tacticId))
{ {
return Unauthorized(); return Unauthorized();
} }
@ -142,7 +142,7 @@ public class TacticController(ITacticService service, IContextAccessor accessor)
public async Task<IActionResult> SaveStepContent(int tacticId, int stepId, [FromBody] SaveStepContentRequest req) public async Task<IActionResult> SaveStepContent(int tacticId, int stepId, [FromBody] SaveStepContentRequest req)
{ {
var userId = accessor.CurrentUserId(HttpContext); var userId = accessor.CurrentUserId(HttpContext);
if (!await service.HasAnyRights(userId, tacticId)) if (!await service.IsOwnerOf(userId, tacticId))
{ {
return Unauthorized(); return Unauthorized();
} }
@ -150,5 +150,28 @@ public class TacticController(ITacticService service, IContextAccessor accessor)
var found = await service.SetTacticStepContent(tacticId, stepId, JsonSerializer.Serialize(req.Content)); var found = await service.SetTacticStepContent(tacticId, stepId, JsonSerializer.Serialize(req.Content));
return found ? Ok() : NotFound(); return found ? Ok() : NotFound();
} }
public record CanEditResponse(bool CanEdit);
[HttpGet("/tactics/{tacticId:int}/can-edit")]
[Authorize]
public async Task<CanEditResponse> CanEdit(int tacticId)
{
var userId = accessor.CurrentUserId(HttpContext);
return new CanEditResponse(await service.IsOwnerOf(userId, tacticId));
}
[HttpDelete("/tactics/{tacticId:int}")]
[Authorize]
public async Task<IActionResult> RemoveTactic(int tacticId)
{
var userId = accessor.CurrentUserId(HttpContext);
if (!await service.IsOwnerOf(userId, tacticId))
{
return Unauthorized();
}
return await service.RemoveTactic(tacticId) ? Ok() : NotFound();
}
} }

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Context; using API.Context;
using API.DTO;
using API.Validation; using API.Validation;
using AppContext.Entities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Model; using Model;
@ -11,13 +11,20 @@ namespace API.Controllers;
[ApiController] [ApiController]
[Authorize] [Authorize]
public class TeamsController(ITeamService service, ITacticService tactics,IContextAccessor accessor) : ControllerBase public class TeamsController(
ITeamService service,
ITacticService tactics,
IUserService users,
IContextAccessor accessor
) : ControllerBase
{ {
public record CreateTeamRequest( public record CreateTeamRequest(
[Name] string Name, [Name] string Name,
[Url] string Picture, [Url] string Picture,
[RegularExpression("^#[0-9A-F]{6}$")] string FirstColor, [RegularExpression("^#[0-9A-Fa-f]{6}$")]
[RegularExpression("^#[0-9A-F]{6}$")] string SecondColor string FirstColor,
[RegularExpression("^#[0-9A-Fa-f]{6}$")]
string SecondColor
); );
[HttpPost("/teams")] [HttpPost("/teams")]
@ -29,6 +36,14 @@ public class TeamsController(ITeamService service, ITacticService tactics,IConte
return Ok(team); return Ok(team);
} }
[HttpGet("/teams/{id:int}")]
public async Task<IActionResult> GetTeam(int id)
{
var team = await service.GetTeam(id);
return team == null ? NotFound() : Ok(team);
}
[HttpGet("/teams/{teamId:int}/members")] [HttpGet("/teams/{teamId:int}/members")]
public async Task<IActionResult> GetMembersOf(int teamId) public async Task<IActionResult> GetMembersOf(int teamId)
{ {
@ -38,7 +53,16 @@ public class TeamsController(ITeamService service, ITacticService tactics,IConte
switch (accessibility) switch (accessibility)
{ {
case ITeamService.TeamAccessibility.Authorized: case ITeamService.TeamAccessibility.Authorized:
return Ok(await service.GetMembersOf(teamId)); var members = (await service.GetMembersOf(teamId)).ToList();
var membersDto = new List<MemberDto>();
foreach (var m in members)
{
membersDto.Add(new MemberDto((await users.GetUser(m.UserId))!, m.Role));
}
return Ok(membersDto);
case ITeamService.TeamAccessibility.NotFound: case ITeamService.TeamAccessibility.NotFound:
case ITeamService.TeamAccessibility.Unauthorized: case ITeamService.TeamAccessibility.Unauthorized:
return NotFound(); return NotFound();
@ -59,7 +83,7 @@ public class TeamsController(ITeamService service, ITacticService tactics,IConte
{ {
throw new Exception($"Unable to convert string input '{req.Role}' to a role enum variant."); throw new Exception($"Unable to convert string input '{req.Role}' to a role enum variant.");
} }
var accessibility = var accessibility =
await service.EnsureAccessibility(accessor.CurrentUserId(HttpContext), teamId, MemberRole.Coach); await service.EnsureAccessibility(accessor.CurrentUserId(HttpContext), teamId, MemberRole.Coach);
@ -111,8 +135,6 @@ public class TeamsController(ITeamService service, ITacticService tactics,IConte
default: //unreachable default: //unreachable
return Problem(); return Problem();
} }
} }
[HttpDelete("/team/{teamId:int}/members/{userId:int}")] [HttpDelete("/team/{teamId:int}/members/{userId:int}")]
@ -135,12 +157,12 @@ public class TeamsController(ITeamService service, ITacticService tactics,IConte
return Problem(); return Problem();
} }
} }
public record ShareTacticToTeamRequest( public record ShareTacticToTeamRequest(
int TacticId, int TacticId,
int TeamId int TeamId
); );
[HttpPost("/team/share-tactic")] [HttpPost("/team/share-tactic")]
public async Task<IActionResult> ShareTactic([FromBody] ShareTacticToTeamRequest sharedTactic) public async Task<IActionResult> ShareTactic([FromBody] ShareTacticToTeamRequest sharedTactic)
{ {
@ -149,7 +171,7 @@ public class TeamsController(ITeamService service, ITacticService tactics,IConte
return success ? Ok() : BadRequest(); return success ? Ok() : BadRequest();
} }
[HttpDelete("/tactics/shared/{tacticId:int}/team/{teamId:int}")] [HttpDelete("/tactics/shared/{tacticId:int}/team/{teamId:int}")]
public async Task<IActionResult> UnshareTactic(int tacticId, int teamId) public async Task<IActionResult> UnshareTactic(int tacticId, int teamId)
{ {
@ -160,6 +182,7 @@ public class TeamsController(ITeamService service, ITacticService tactics,IConte
{ {
return NotFound(); return NotFound();
} }
if (currentUserId != tactic.OwnerId) if (currentUserId != tactic.OwnerId)
{ {
return Unauthorized(); return Unauthorized();
@ -168,12 +191,12 @@ public class TeamsController(ITeamService service, ITacticService tactics,IConte
var success = await tactics.UnshareTactic(tacticId, null, teamId); var success = await tactics.UnshareTactic(tacticId, null, teamId);
return success ? Ok() : NotFound(); return success ? Ok() : NotFound();
} }
[HttpGet("/tactics/shared/team/{teamId:int}")] [HttpGet("/tactics/shared/team/{teamId:int}")]
public async Task<IActionResult> GetSharedTacticsToTeam(int teamId) public async Task<IActionResult> GetSharedTacticsToTeam(int teamId)
{ {
var currentUserId = accessor.CurrentUserId(HttpContext); var currentUserId = accessor.CurrentUserId(HttpContext);
if (!await service.IsUserInTeam(currentUserId, teamId)) if (!await service.IsUserInTeam(currentUserId, teamId))
{ {
return Unauthorized(); return Unauthorized();

@ -1,13 +1,15 @@
using System.ComponentModel.DataAnnotations;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using API.Context; using API.Context;
using API.DTO; using API.DTO;
using AppContext.Entities; using API.Validation;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Model; using Model;
using Services; using Services;
[assembly: InternalsVisibleTo("UnitTests")] [assembly: InternalsVisibleTo("UnitTests")]
namespace API.Controllers; namespace API.Controllers;
[ApiController] [ApiController]
@ -36,19 +38,56 @@ public class UsersController(IUserService users, ITeamService teams, ITacticServ
var userTactics = await tactics.ListTacticsOf(userId); var userTactics = await tactics.ListTacticsOf(userId);
return new GetUserDataResponse(userTeams.ToArray(), userTactics.Select(t => t.ToDto()).ToArray()); return new GetUserDataResponse(userTeams.ToArray(), userTactics.Select(t => t.ToDto()).ToArray());
} }
public record ChangeUserInformationRequest(
[EmailAddress] string? Email = null,
[Name] string? Name = null,
[StringLength(1024)] string? ProfilePicture = null,
[StringLength(256, MinimumLength = 4, ErrorMessage = "password length must be between 4 and 256")]
string? Password = null
);
[HttpPut("/user")]
[Authorize]
public async Task<IActionResult> ChangeUserInformation([FromBody] ChangeUserInformationRequest req)
{
var userId = accessor.CurrentUserId(HttpContext);
var currentUser = (await users.GetUser(userId))!;
try
{
await users.UpdateUser(
new User(
userId,
req.Name ?? currentUser.Name,
req.Email ?? currentUser.Email,
req.ProfilePicture ?? currentUser.ProfilePicture,
currentUser.IsAdmin
),
req.Password
);
} catch (ServiceException e)
{
return BadRequest(e.FailuresMessages());
}
return Ok();
}
public record ShareTacticToUserRequest( public record ShareTacticToUserRequest(
int TacticId, int TacticId,
int UserId int UserId
); );
[HttpPost("/user/share-tactic")] [HttpPost("/user/share-tactic")]
[Authorize] [Authorize]
public async Task<IActionResult> ShareTactic([FromBody] ShareTacticToUserRequest sharedTactic) public async Task<IActionResult> ShareTactic([FromBody] ShareTacticToUserRequest sharedTactic)
{ {
var currentUserId = accessor.CurrentUserId(HttpContext); var currentUserId = accessor.CurrentUserId(HttpContext);
var tactic = await tactics.GetTactic(sharedTactic.TacticId); var tactic = await tactics.GetTactic(sharedTactic.TacticId);
if (tactic == null) if (tactic == null)
{ {
return NotFound(); return NotFound();
@ -62,7 +101,7 @@ public class UsersController(IUserService users, ITeamService teams, ITacticServ
var result = await tactics.ShareTactic(sharedTactic.TacticId, sharedTactic.UserId, null); var result = await tactics.ShareTactic(sharedTactic.TacticId, sharedTactic.UserId, null);
return result ? Ok() : NotFound(); return result ? Ok() : NotFound();
} }
[HttpDelete("/tactics/shared/{tacticId:int}/user/{userId:int}")] [HttpDelete("/tactics/shared/{tacticId:int}/user/{userId:int}")]
[Authorize] [Authorize]
public async Task<IActionResult> UnshareTactic(int tacticId, int userId) public async Task<IActionResult> UnshareTactic(int tacticId, int userId)
@ -74,6 +113,7 @@ public class UsersController(IUserService users, ITeamService teams, ITacticServ
{ {
return NotFound(); return NotFound();
} }
if (currentUserId != tactic.OwnerId) if (currentUserId != tactic.OwnerId)
{ {
return Unauthorized(); return Unauthorized();
@ -82,7 +122,7 @@ public class UsersController(IUserService users, ITeamService teams, ITacticServ
var success = await tactics.UnshareTactic(tacticId, userId, null); var success = await tactics.UnshareTactic(tacticId, userId, null);
return success ? Ok() : NotFound(); return success ? Ok() : NotFound();
} }
[HttpGet("/tactics/shared/user/{userId:int}")] [HttpGet("/tactics/shared/user/{userId:int}")]
[Authorize] [Authorize]
public async Task<IActionResult> GetSharedTacticsToUser(int userId) public async Task<IActionResult> GetSharedTacticsToUser(int userId)
@ -94,6 +134,6 @@ public class UsersController(IUserService users, ITeamService teams, ITacticServ
} }
var sharedTactics = await users.GetSharedTacticsToUser(userId); var sharedTactics = await users.GetSharedTacticsToUser(userId);
return sharedTactics != null ? Ok(sharedTactics) : NotFound(); return Ok(sharedTactics);
} }
} }

@ -0,0 +1,5 @@
using Model;
namespace API.DTO;
public record MemberDto(User User, MemberRole Role);

@ -101,7 +101,7 @@ app.Use((context, next) =>
var (jwt, expirationDate) = Authentication.GenerateJwt(key, context.User.Claims); var (jwt, expirationDate) = Authentication.GenerateJwt(key, context.User.Claims);
context.Response.Headers["Next-Authorization"] = jwt; context.Response.Headers["Next-Authorization"] = jwt;
context.Response.Headers["Next-Authorization-Expiration-Date"] = context.Response.Headers["Next-Authorization-Expiration-Date"] =
expirationDate.ToString(CultureInfo.InvariantCulture); expirationDate.ToFileTimeUtc().ToString();
context.Response.Headers.AccessControlExposeHeaders = "Next-Authorization, Next-Authorization-Expiration-Date"; context.Response.Headers.AccessControlExposeHeaders = "Next-Authorization, Next-Authorization-Expiration-Date";
return next.Invoke(); return next.Invoke();
}); });

@ -9,6 +9,9 @@ public partial class NameAttribute : ValidationAttribute
{ {
var name = context.DisplayName; var name = context.DisplayName;
if (value is null)
return ValidationResult.Success;
if (value is not string str) if (value is not string str)
{ {
return new ValidationResult($"{name} should be a string."); return new ValidationResult($"{name} should be a string.");

@ -17,8 +17,9 @@ namespace APIConsole
AppContext.AppContext context = new AppContext.AppContext(); AppContext.AppContext context = new AppContext.AppContext();
ITeamService teams = new DbTeamService(context); ITeamService teams = new DbTeamService(context);
ITacticService tactics = new DbTacticService(context); ITacticService tactics = new DbTacticService(context);
IUserService users = new DbUserService(context);
IContextAccessor accessor = new HttpContextAccessor(); IContextAccessor accessor = new HttpContextAccessor();
_controller = new TeamsController(teams, tactics, accessor); _controller = new TeamsController(teams, tactics, users, accessor);
} }
public async void GetMembersOfTest() public async void GetMembersOfTest()

@ -46,26 +46,9 @@ public class AppContext : DbContext
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {
base.OnModelCreating(builder); base.OnModelCreating(builder);
builder.Entity<UserEntity>()
.Property(e => e.Password)
.HasConversion(
v => HashString(v),
v => v
);
builder.Entity<MemberEntity>() builder.Entity<MemberEntity>()
.HasKey("UserId", "TeamId"); .HasKey("UserId", "TeamId");
} }
private static string HashString(string str)
{
byte[] salt = RandomNumberGenerator.GetBytes(128 / 8);
return Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: str,
salt,
prf: KeyDerivationPrf.HMACSHA256,
iterationCount: 50000,
numBytesRequested: 256 / 8
));
}
} }

@ -9,7 +9,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

@ -7,6 +7,7 @@ public class UserEntity
[Key] public int Id { get; set; } [Key] public int Id { get; set; }
public required string Password { get; set; } public required string Password { get; set; }
public required byte[] PasswordSalt { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
public required string Email { get; set; } public required string Email { get; set; }
public required string ProfilePicture { get; set; } public required string ProfilePicture { get; set; }

@ -22,7 +22,7 @@ public static class EntitiesToModels
entity.Id, entity.Id,
entity.ParentId, entity.ParentId,
steps.Where(s =>s.TacticId == entity.TacticId && s.ParentId == entity.Id) steps.Where(s =>s.TacticId == entity.TacticId && s.ParentId == entity.Id)
.AsEnumerable() .ToList()
.Select(e => e.ToModel(steps)), .Select(e => e.ToModel(steps)),
entity.JsonContent entity.JsonContent
); );

@ -10,6 +10,7 @@
<ProjectReference Include="..\AppContext\AppContext.csproj" /> <ProjectReference Include="..\AppContext\AppContext.csproj" />
<ProjectReference Include="..\Converters\Converters.csproj" /> <ProjectReference Include="..\Converters\Converters.csproj" />
<ProjectReference Include="..\Services\Services.csproj" /> <ProjectReference Include="..\Services\Services.csproj" />
<ProjectReference Include="..\Utils\Utils.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -18,7 +18,7 @@ public class DbTacticService(AppContext.AppContext context) : ITacticService
); );
} }
public async Task<bool> HasAnyRights(int userId, int tacticId) public async Task<bool> IsOwnerOf(int userId, int tacticId)
{ {
var tacticEntity = await context.Tactics.FirstOrDefaultAsync(u => u.Id == tacticId); var tacticEntity = await context.Tactics.FirstOrDefaultAsync(u => u.Id == tacticId);
if (tacticEntity == null) if (tacticEntity == null)
@ -27,7 +27,7 @@ public class DbTacticService(AppContext.AppContext context) : ITacticService
return tacticEntity.OwnerId == userId; return tacticEntity.OwnerId == userId;
} }
public async Task<int> AddTactic(int userId, string name, CourtType courtType) public async Task<(int, int)> AddTactic(int userId, string name, CourtType courtType)
{ {
var tacticEntity = new TacticEntity var tacticEntity = new TacticEntity
{ {
@ -49,7 +49,7 @@ public class DbTacticService(AppContext.AppContext context) : ITacticService
await context.SaveChangesAsync(); await context.SaveChangesAsync();
return tacticEntity.Id; return (tacticEntity.Id, stepEntity.Id);
} }
public async Task<bool> UpdateName(int tacticId, string name) public async Task<bool> UpdateName(int tacticId, string name)
@ -195,4 +195,15 @@ public class DbTacticService(AppContext.AppContext context) : ITacticService
context.SharedTactics.Remove(sharedTactic); context.SharedTactics.Remove(sharedTactic);
return await context.SaveChangesAsync() > 0; return await context.SaveChangesAsync() > 0;
} }
public async Task<bool> RemoveTactic(int tacticId)
{
var removed = await context.Tactics.Where(t => t.Id == tacticId).ExecuteDeleteAsync() > 0;
if (!removed)
return false;
await context.TacticSteps.Where(s => s.TacticId == tacticId).ExecuteDeleteAsync();
return true;
}
} }

@ -52,6 +52,12 @@ public class DbTeamService(AppContext.AppContext context) : ITeamService
return entity.ToModel(); return entity.ToModel();
} }
public async Task<Team?> GetTeam(int id)
{
var entity = await context.Teams.FirstOrDefaultAsync(t => t.Id == id);
return entity?.ToModel();
}
public async Task RemoveTeams(params int[] teams) public async Task RemoveTeams(params int[] teams)
{ {
await context.Teams await context.Teams

@ -22,12 +22,11 @@ public class DbUserService(AppContext.AppContext context) : IUserService
public Task<IEnumerable<User>> ListUsers(int start, int count, string? nameNeedle = null) public Task<IEnumerable<User>> ListUsers(int start, int count, string? nameNeedle = null)
{ {
IQueryable<UserEntity> request = context.Users; IQueryable<UserEntity> request = context.Users;
if (nameNeedle != null) if (nameNeedle != null)
request = request.Where(u => u.Name.ToLower().Contains(nameNeedle.ToLower())); request = request.Where(u => u.Name.ToLower().Contains(nameNeedle.ToLower()));
return Task.FromResult( return Task.FromResult(
request request
.Skip(start) .Skip(start)
@ -46,15 +45,23 @@ public class DbUserService(AppContext.AppContext context) : IUserService
{ {
return (await context.Users.FirstOrDefaultAsync(e => e.Email == email))?.ToModel(); return (await context.Users.FirstOrDefaultAsync(e => e.Email == email))?.ToModel();
} }
public async Task<User> CreateUser(string username, string email, string password, string profilePicture, public async Task<User> CreateUser(
bool isAdmin) string username,
string email,
string password,
string profilePicture,
bool isAdmin
)
{ {
var (passwordHash, salt) = Hashing.HashString(password);
var userEntity = new UserEntity var userEntity = new UserEntity
{ {
Name = username, Name = username,
Email = email, Email = email,
Password = password, Password = passwordHash,
PasswordSalt = salt,
ProfilePicture = profilePicture, ProfilePicture = profilePicture,
IsAdmin = isAdmin IsAdmin = isAdmin
}; };
@ -73,27 +80,44 @@ public class DbUserService(AppContext.AppContext context) : IUserService
.ExecuteDeleteAsync() > 0; .ExecuteDeleteAsync() > 0;
} }
public async Task UpdateUser(User user) public async Task UpdateUser(User user, string? password = null)
{ {
var entity = await context.Users.FirstOrDefaultAsync(e => e.Id == user.Id); var entity = await context.Users.FirstOrDefaultAsync(e => e.Id == user.Id);
if (entity == null) if (entity == null)
throw new ServiceException(Failure.NotFound("User not found")); throw new ServiceException(Failure.NotFound("User not found"));
var emailEntity = await context.Users.FirstOrDefaultAsync(e => e.Email == user.Email);
if (emailEntity != null && emailEntity.Id != entity.Id)
{
throw new ServiceException(new Failure("email conflict", "this provided email is used by another account"));
}
entity.ProfilePicture = user.ProfilePicture; entity.ProfilePicture = user.ProfilePicture;
entity.Name = user.Name; entity.Name = user.Name;
entity.Email = user.Email; entity.Email = user.Email;
entity.Id = user.Id; entity.Id = user.Id;
entity.IsAdmin = user.IsAdmin; entity.IsAdmin = user.IsAdmin;
if (password != null)
{
var (passwordHash, salt) = Hashing.HashString(password);
entity.Password = passwordHash;
entity.PasswordSalt = salt;
}
await context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
public async Task<User?> Authorize(string email, string password) public async Task<User?> Authorize(string email, string password)
{ {
return (await context var entity = await context
.Users .Users
.FirstOrDefaultAsync(u => u.Email == email)) .FirstOrDefaultAsync(u => u.Email == email);
?.ToModel();
if (entity == null)
return null;
return Hashing.PasswordsMatches(entity.Password, password, entity.PasswordSalt) ? entity.ToModel() : null;
} }
public async Task<IEnumerable<Tactic>> GetSharedTacticsToUser(int userId) public async Task<IEnumerable<Tactic>> GetSharedTacticsToUser(int userId)

@ -1,3 +1,6 @@
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
namespace DbServices; namespace DbServices;
public class Security public class Security

@ -12,6 +12,7 @@ class UsersConsole
Name = "Pierre", Name = "Pierre",
Email = "pierre@mail.com", Email = "pierre@mail.com",
Password = "123456", Password = "123456",
PasswordSalt = [1],
ProfilePicture = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png", ProfilePicture = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png",
IsAdmin = false IsAdmin = false
}; };

@ -15,12 +15,12 @@ public interface ITacticService
Task<IEnumerable<Tactic>> ListTacticsOf(int userId); Task<IEnumerable<Tactic>> ListTacticsOf(int userId);
/// <summary> /// <summary>
/// Checks if the user has any rights to access the specified tactic. /// Checks if the userId corresponds to the tactic's owner identifier
/// </summary> /// </summary>
/// <param name="userId">The ID of the user.</param> /// <param name="userId">The ID of the user.</param>
/// <param name="tacticId">The ID of the tactic.</param> /// <param name="tacticId">The ID of the tactic.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean indicating whether the user has rights.</returns> /// <returns>A task that represents the asynchronous operation. The task result contains a boolean indicating whether the user has rights.</returns>
Task<bool> HasAnyRights(int userId, int tacticId); Task<bool> IsOwnerOf(int userId, int tacticId);
/// <summary> /// <summary>
/// Adds a new tactic for the specified user. /// Adds a new tactic for the specified user.
@ -29,7 +29,7 @@ public interface ITacticService
/// <param name="name">The name of the tactic.</param> /// <param name="name">The name of the tactic.</param>
/// <param name="courtType">The type of court.</param> /// <param name="courtType">The type of court.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the ID of the newly added tactic.</returns> /// <returns>A task that represents the asynchronous operation. The task result contains the ID of the newly added tactic.</returns>
Task<int> AddTactic(int userId, string name, CourtType courtType); Task<(int, int)> AddTactic(int userId, string name, CourtType courtType);
/// <summary> /// <summary>
/// Updates the name of the specified tactic. /// Updates the name of the specified tactic.
@ -98,4 +98,5 @@ public interface ITacticService
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean indicating whether the removal was successful.</returns> /// <returns>A task that represents the asynchronous operation. The task result contains a boolean indicating whether the removal was successful.</returns>
Task<bool> RemoveTacticStep(int tacticId, int stepId); Task<bool> RemoveTacticStep(int tacticId, int stepId);
Task<bool> RemoveTactic(int tacticId);
} }

@ -26,6 +26,8 @@ public interface ITeamService
/// Adds a new team. /// Adds a new team.
/// </summary> /// </summary>
Task<Team> AddTeam(string name, string picture, string firstColor, string secondColor); Task<Team> AddTeam(string name, string picture, string firstColor, string secondColor);
Task<Team?> GetTeam(int id);
/// <summary> /// <summary>
/// Removes one or more teams. /// Removes one or more teams.

@ -10,4 +10,11 @@ public class ServiceException : Exception
{ {
Failures = new List<Failure>(failures); Failures = new List<Failure>(failures);
} }
public Dictionary<string, string[]> FailuresMessages()
{
return Failures.GroupBy(f => f.Name)
.Select(f => (f.Key, f.Select(f => f.Message).ToArray()))
.ToDictionary();
}
} }

@ -45,7 +45,7 @@ public interface IUserService
/// <summary> /// <summary>
/// Updates an existing user. /// Updates an existing user.
/// </summary> /// </summary>
Task UpdateUser(User user); Task UpdateUser(User user, string? password = null);
public Task<IEnumerable<Tactic>> GetSharedTacticsToUser(int userId); public Task<IEnumerable<Tactic>> GetSharedTacticsToUser(int userId);

@ -1,4 +1,5 @@
using AppContext.Entities; using AppContext.Entities;
using DbServices;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Model; using Model;
@ -24,15 +25,20 @@ public class StubAppContext(DbContextOptions<AppContext> options) : AppContext(o
var i = 0; var i = 0;
builder.Entity<UserEntity>() builder.Entity<UserEntity>()
.HasData(users.ConvertAll(name => new UserEntity .HasData(users.ConvertAll(name =>
{ {
Id = ++i, var (password, salt) = Hashing.HashString("123456");
Email = $"{name}@mail.com", return new UserEntity
Name = name, {
Password = "123456", Id = ++i,
IsAdmin = true, Email = $"{name}@mail.com",
ProfilePicture = Name = name,
"https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png", Password = password,
PasswordSalt = salt,
IsAdmin = true,
ProfilePicture =
"https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png",
};
})); }));
builder.Entity<TacticEntity>() builder.Entity<TacticEntity>()

@ -9,6 +9,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\AppContext\AppContext.csproj" /> <ProjectReference Include="..\AppContext\AppContext.csproj" />
<ProjectReference Include="..\Utils\Utils.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

@ -71,7 +71,7 @@ public class TacticsControllerTest
{ {
var (controller, context) = GetController(1); var (controller, context) = GetController(1);
var result = await controller.CreateNew(new("Test Tactic", "pLaIn")); var result = await controller.CreateNew(new("Test Tactic", "pLaIn"));
result.Should().BeEquivalentTo(new TacticController.CreateNewResponse(2)); result.Should().BeEquivalentTo(new TacticController.CreateNewResponse(2, 2));
var tactic = await context.Tactics.FirstOrDefaultAsync(e => e.Id == 2); var tactic = await context.Tactics.FirstOrDefaultAsync(e => e.Id == 2);
tactic.Should().NotBeNull(); tactic.Should().NotBeNull();
tactic!.Name.Should().BeEquivalentTo("Test Tactic"); tactic!.Name.Should().BeEquivalentTo("Test Tactic");

@ -21,7 +21,9 @@ public class TeamsControllerTest
); );
context.Database.EnsureCreated(); context.Database.EnsureCreated();
var controller = new TeamsController( var controller = new TeamsController(
new DbTeamService(context), new DbTacticService(context), new DbTeamService(context),
new DbTacticService(context),
new DbUserService(context),
new ManualContextAccessor(userId) new ManualContextAccessor(userId)
); );
@ -45,11 +47,11 @@ public class TeamsControllerTest
{ {
var (controller, context) = GetController(1); var (controller, context) = GetController(1);
var result = await controller.GetMembersOf(1); var result = await controller.GetMembersOf(1);
result.Should().BeEquivalentTo(controller.Ok(new Member[] // result.Should().BeEquivalentTo(controller.Ok(new Member[]
{ // {
new(1, 1, MemberRole.Coach), // new(1, 1, MemberRole.Coach),
new(1, 2, MemberRole.Player) // new(1, 2, MemberRole.Player)
})); // }));
} }
[Fact] [Fact]

@ -66,9 +66,9 @@ public class UsersControllerTest
var result = await controller.GetSharedTacticsToUser(2); var result = await controller.GetSharedTacticsToUser(2);
var okResult = result as OkObjectResult; var okResult = result as OkObjectResult;
var sharedTactics = okResult.Value as IEnumerable<Tactic>; var sharedTactics = okResult!.Value as IEnumerable<Tactic>;
sharedTactics.Should().NotBeNull(); sharedTactics!.Should().NotBeNull();
sharedTactics.Should().ContainSingle(); sharedTactics.Should().ContainSingle();
var tactic = sharedTactics.First(); var tactic = sharedTactics.First();
@ -83,4 +83,13 @@ public class UsersControllerTest
result.Should().BeOfType<OkResult>(); result.Should().BeOfType<OkResult>();
} }
[Fact]
public async Task TestChangeUserInformation()
{
var controller = GetUserController(1);
await controller.ChangeUserInformation(new("a", "b", "c", "d"));
var user = await controller.GetUser();
user.Should().BeEquivalentTo(new User(1, "b", "a", "c", true));
}
} }

@ -0,0 +1,35 @@
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
namespace DbServices;
public class Hashing
{
public static (string, byte[]) HashString(string str)
{
byte[] salt = RandomNumberGenerator.GetBytes(128 / 8);
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: str,
salt,
prf: KeyDerivationPrf.HMACSHA256,
iterationCount: 50000,
numBytesRequested: 256 / 8
));
return (hashed, salt);
}
public static bool PasswordsMatches(string password, string str, byte[] salt)
{
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: str,
salt,
prf: KeyDerivationPrf.HMACSHA256,
iterationCount: 50000,
numBytesRequested: 256 / 8
));
return hashed == password;
}
}

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="9.0.0-preview.2.24128.4" />
</ItemGroup>
</Project>

@ -20,6 +20,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFConsole", "EFConsole\EFCo
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "APIConsole", "APIConsole\APIConsole.csproj", "{B01BD72E-15D3-4DC6-8DAC-2270A01129A9}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "APIConsole", "APIConsole\APIConsole.csproj", "{B01BD72E-15D3-4DC6-8DAC-2270A01129A9}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utils", "Utils\Utils.csproj", "{D6FC4ED1-B4F8-4801-BC79-94627A1E6E0F}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -74,5 +76,9 @@ Global
{B01BD72E-15D3-4DC6-8DAC-2270A01129A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {B01BD72E-15D3-4DC6-8DAC-2270A01129A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B01BD72E-15D3-4DC6-8DAC-2270A01129A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {B01BD72E-15D3-4DC6-8DAC-2270A01129A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B01BD72E-15D3-4DC6-8DAC-2270A01129A9}.Release|Any CPU.Build.0 = Release|Any CPU {B01BD72E-15D3-4DC6-8DAC-2270A01129A9}.Release|Any CPU.Build.0 = Release|Any CPU
{D6FC4ED1-B4F8-4801-BC79-94627A1E6E0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D6FC4ED1-B4F8-4801-BC79-94627A1E6E0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D6FC4ED1-B4F8-4801-BC79-94627A1E6E0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D6FC4ED1-B4F8-4801-BC79-94627A1E6E0F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

@ -2,7 +2,7 @@ kind: pipeline
type: docker type: docker
name: "CI/CD" name: "CI/CD"
steps: steps:
- image: mcr.microsoft.com/dotnet/sdk:8.0 - image: mcr.microsoft.com/dotnet/sdk:8.0
@ -12,12 +12,12 @@ steps:
- dotnet tool install --global dotnet-sonarscanner - dotnet tool install --global dotnet-sonarscanner
- dotnet tool install --global dotnet-coverage - dotnet tool install --global dotnet-coverage
- export PATH="$PATH:/root/.dotnet/tools" - export PATH="$PATH:/root/.dotnet/tools"
- dotnet sonarscanner begin /k:"IQBall-WebAPI" /d:sonar.host.url="https://codefirst.iut.uca.fr/sonar" /d:sonar.login="sqp_b16ad09dcce1b9dde920e313b10c2fe85566624c" - dotnet sonarscanner begin /k:"IQBall-WebAPI" /d:sonar.host.url="https://codefirst.iut.uca.fr/sonar" /d:sonar.login="sqp_b16ad09dcce1b9dde920e313b10c2fe85566624c" /d:sonar.cs.vscoveragexml.reportsPaths=coverage.xml
- dotnet build - dotnet build
- dotnet-coverage collect "dotnet test" -f xml -o "coverage.xml" - dotnet-coverage collect "dotnet test" -f xml -o coverage.xml
- dotnet sonarscanner end /d:sonar.login="sqp_b16ad09dcce1b9dde920e313b10c2fe85566624c" - dotnet sonarscanner end /d:sonar.login="sqp_b16ad09dcce1b9dde920e313b10c2fe85566624c"
- image: plugins/docker - image: plugins/docker
name: "build and push docker image" name: "build and push docker image"
depends_on: depends_on:
@ -33,23 +33,20 @@ steps:
from_secret: SECRET_REGISTRY_USERNAME from_secret: SECRET_REGISTRY_USERNAME
password: password:
from_secret: SECRET_REGISTRY_PASSWORD from_secret: SECRET_REGISTRY_PASSWORD
# deploy staging database and server on codefirst # deploy staging database and server on codefirst
- image: eeacms/rsync:latest - image: ubuntu:latest
name: "Instantiate docker images on staging server" name: "Instantiate docker images on staging server"
depends_on: depends_on:
- "build and push docker image" - "build and push docker image"
environment: environment:
PRIVATE_KEY: PRIVATE_KEY:
from_secret: PRIVATE_KEY from_secret: PRIVATE_KEY
commands: commands:
- mkdir -p ~/.ssh - chmod +x ci/deploy_staging_server_step.sh
- echo "$PRIVATE_KEY" > ~/.ssh/id_rsa - ci/deploy_staging_server_step.sh
- chmod 0600 ~/.ssh
- chmod 0500 ~/.ssh/id_rsa*
- rsync -avz -e "ssh -p 80 -o 'StrictHostKeyChecking=no'" ci/deploy_staging_server.sh iqball@maxou.dev:/srv/www/iqball/$DRONE_BRANCH/
- ssh -p 80 -o 'StrictHostKeyChecking=no' iqball@maxou.dev "chmod +x /srv/www/iqball/$DRONE_BRANCH/deploy_staging_server.sh && /srv/www/iqball/$DRONE_BRANCH/deploy_staging_server.sh $(echo $DRONE_BRANCH | tr / _) $DRONE_COMMIT_SHA"
# Deploy the production database and server on codefirst # Deploy the production database and server on codefirst
# - image: hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-dockerproxy-clientdrone:latest # - image: hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-dockerproxy-clientdrone:latest
# name: "Instantiate dotnet api docker image on codefirst" # name: "Instantiate dotnet api docker image on codefirst"

@ -3,13 +3,14 @@
set -exu set -exu
BRANCH=$1 BRANCH=$1
BRANCH_ESCAPED=$(echo $BRANCH | tr / _)
COMMIT_SHA=$2 COMMIT_SHA=$2
API_CONTAINER_NAME="iqball-api-dotnet-$BRANCH" API_CONTAINER_NAME="iqball-api-dotnet-$BRANCH_ESCAPED"
DB_CONTAINER_NAME="iqball-db-$BRANCH" DB_CONTAINER_NAME="iqball-db-$BRANCH_ESCAPED"
(docker stop "$API_CONTAINER_NAME" && docker rm "$API_CONTAINER_NAME") || true (docker stop "$API_CONTAINER_NAME" && docker rm "$API_CONTAINER_NAME") || true
docker volume create "iqball-migrations-$BRANCH" || true docker volume create "iqball-migrations-$BRANCH_ESCAPED" || true
docker run -d \ docker run -d \
--name "$DB_CONTAINER_NAME" \ --name "$DB_CONTAINER_NAME" \
@ -25,12 +26,12 @@ docker run --rm -t \
--env PGSQL_DSN="Server=$DB_CONTAINER_NAME;Username=iqball;Password=1234;Database=iqball" \ --env PGSQL_DSN="Server=$DB_CONTAINER_NAME;Username=iqball;Password=1234;Database=iqball" \
--env BRANCH="$BRANCH" \ --env BRANCH="$BRANCH" \
--env COMMIT_SHA="$COMMIT_SHA" \ --env COMMIT_SHA="$COMMIT_SHA" \
--mount source="iqball-migrations-$BRANCH",target=/migrations \ --mount source="iqball-migrations-$BRANCH_ESCAPED",target=/migrations \
--network iqball_net \ --network iqball_net \
iqball-db-init:latest iqball-db-init:latest
docker pull "hub.codefirst.iut.uca.fr/maxime.batista/iqball-api-dotnet:$BRANCH" docker pull "hub.codefirst.iut.uca.fr/maxime.batista/iqball-api-dotnet:$BRANCH_ESCAPED"
# run the API # run the API
docker run -d \ docker run -d \
@ -38,7 +39,7 @@ docker run -d \
--restart=always \ --restart=always \
--network iqball_net \ --network iqball_net \
--env PGSQL_DSN="Server=$DB_CONTAINER_NAME;Username=iqball;Password=1234;Database=iqball" \ --env PGSQL_DSN="Server=$DB_CONTAINER_NAME;Username=iqball;Password=1234;Database=iqball" \
"hub.codefirst.iut.uca.fr/maxime.batista/iqball-api-dotnet:$BRANCH" "hub.codefirst.iut.uca.fr/maxime.batista/iqball-api-dotnet:$BRANCH_ESCAPED"

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -exu
apt update && apt install rsync openssh-client -y
mkdir -p ~/.ssh
echo "$PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 0600 ~/.ssh
chmod 0500 ~/.ssh/id_rsa*
ssh -p 80 -o 'StrictHostKeyChecking=no' iqball@maxou.dev mkdir -p /srv/www/iqball/$DRONE_BRANCH/
rsync -avz -e "ssh -p 80 -o 'StrictHostKeyChecking=no'" ci/deploy_staging_server.sh iqball@maxou.dev:/srv/www/iqball/$DRONE_BRANCH/
ssh -p 80 -o 'StrictHostKeyChecking=no' iqball@maxou.dev "chmod +x /srv/www/iqball/$DRONE_BRANCH/deploy_staging_server.sh && /srv/www/iqball/$DRONE_BRANCH/deploy_staging_server.sh $DRONE_BRANCH $DRONE_COMMIT_SHA"
Loading…
Cancel
Save