diff --git a/CleanArchitecture/CleanArchitecture.API/CleanArchitecture.API.csproj b/CleanArchitecture/CleanArchitecture.API/CleanArchitecture.API.csproj
index 7b2a188..c854523 100644
--- a/CleanArchitecture/CleanArchitecture.API/CleanArchitecture.API.csproj
+++ b/CleanArchitecture/CleanArchitecture.API/CleanArchitecture.API.csproj
@@ -7,12 +7,17 @@
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
diff --git a/CleanArchitecture/CleanArchitecture.API/Controllers/AccountController.cs b/CleanArchitecture/CleanArchitecture.API/Controllers/AccountController.cs
new file mode 100644
index 0000000..903246f
--- /dev/null
+++ b/CleanArchitecture/CleanArchitecture.API/Controllers/AccountController.cs
@@ -0,0 +1,34 @@
+using CleanArchitecture.Application.Contracts.Identity;
+using CleanArchitecture.Application.Models.Identity;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Identity.Data;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CleanArchitecture.API.Controllers
+{
+ [Route("api/v1/[controller]")]
+ [ApiController]
+ public class AccountController : ControllerBase
+ {
+ private readonly IAuthService authService;
+
+ public AccountController(IAuthService _authService)
+ {
+ authService = _authService;
+ }
+
+ [HttpPost("login")]
+ public async Task> Login([FromBody] AuthRequest request)
+ {
+ var response = await authService.Login(request);
+ return Ok(response);
+ }
+
+ [HttpPost("register")]
+ public async Task> Register([FromBody] RegistrationRequest request)
+ {
+ var response = await authService.Register(request);
+ return Ok(response);
+ }
+ }
+}
diff --git a/CleanArchitecture/CleanArchitecture.API/Controllers/StreamerController.cs b/CleanArchitecture/CleanArchitecture.API/Controllers/StreamerController.cs
index f0c6cec..1128e83 100644
--- a/CleanArchitecture/CleanArchitecture.API/Controllers/StreamerController.cs
+++ b/CleanArchitecture/CleanArchitecture.API/Controllers/StreamerController.cs
@@ -2,6 +2,7 @@
using CleanArchitecture.Application.Features.Streamers.Commands.DeleteStreamer;
using CleanArchitecture.Application.Features.Streamers.Commands.UpdateStreamer;
using MediatR;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Net;
@@ -13,20 +14,22 @@ namespace CleanArchitecture.API.Controllers
public class StreamerController : ControllerBase
{
private readonly IMediator mediator;
- public StreamerController(IMediator _mediator)
- {
- mediator = _mediator;
+ public StreamerController(IMediator _mediator)
+ {
+ mediator = _mediator;
}
[HttpPost(Name = "CreateStreamer")]
+ [Authorize(Roles = "Administrator")]
[ProducesResponseType(typeof(int), (int)HttpStatusCode.OK)]
public async Task> CreateStreamer([FromBody] CreateStreamerCommand command)
{
var response = await mediator.Send(command);
- return Ok(response);
+ return Ok(new { StreamerId = response });
}
[HttpPut(Name = "UpdateStreamer")]
+ [Authorize(Roles = "Administrator")]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesDefaultResponseType]
@@ -37,6 +40,7 @@ namespace CleanArchitecture.API.Controllers
}
[HttpDelete("{id}", Name = "DeleteStreamer")]
+ [Authorize(Roles = "Administrator")]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesDefaultResponseType]
diff --git a/CleanArchitecture/CleanArchitecture.API/Controllers/VideoController.cs b/CleanArchitecture/CleanArchitecture.API/Controllers/VideoController.cs
index 3d4e48e..c5a464b 100644
--- a/CleanArchitecture/CleanArchitecture.API/Controllers/VideoController.cs
+++ b/CleanArchitecture/CleanArchitecture.API/Controllers/VideoController.cs
@@ -1,5 +1,6 @@
using CleanArchitecture.Application.Features.Videos.Queries.GetVideosList;
using MediatR;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Net;
@@ -18,6 +19,7 @@ namespace CleanArchitecture.API.Controllers
}
[HttpGet("{username}", Name = "GetVideo")]
+ [Authorize]
[ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)]
public async Task>> GetVideosByUserName(string username)
{
diff --git a/CleanArchitecture/CleanArchitecture.API/Errors/CodeErrorException.cs b/CleanArchitecture/CleanArchitecture.API/Errors/CodeErrorException.cs
new file mode 100644
index 0000000..e665036
--- /dev/null
+++ b/CleanArchitecture/CleanArchitecture.API/Errors/CodeErrorException.cs
@@ -0,0 +1,13 @@
+namespace CleanArchitecture.API.Errors
+{
+ public class CodeErrorException : CodeErrorResponse
+ {
+
+ public string? Details { get; set; }
+ public CodeErrorException(int statusCode, string? message = null, string? details = null) : base(statusCode, message)
+ {
+
+ Details = details;
+ }
+ }
+}
diff --git a/CleanArchitecture/CleanArchitecture.API/Errors/CodeErrorResponse.cs b/CleanArchitecture/CleanArchitecture.API/Errors/CodeErrorResponse.cs
new file mode 100644
index 0000000..9ba1ef0
--- /dev/null
+++ b/CleanArchitecture/CleanArchitecture.API/Errors/CodeErrorResponse.cs
@@ -0,0 +1,26 @@
+namespace CleanArchitecture.API.Errors
+{
+ public class CodeErrorResponse
+ {
+ public int StatusCode { get; set; }
+ public string? Message { get; set; }
+
+ public CodeErrorResponse(int statusCode, string? message = null)
+ {
+ StatusCode = statusCode;
+ Message = message ?? GetDefaultMessageStatusCode(statusCode);
+ }
+
+ private string GetDefaultMessageStatusCode(int statusCode)
+ {
+ return statusCode switch
+ {
+ 400 => "The request sent have errors",
+ 401 => "You are not authorized",
+ 404 => "Resource not found",
+ 500 => "An internal server error occurred",
+ _ => string.Empty
+ };
+ }
+ }
+}
diff --git a/CleanArchitecture/CleanArchitecture.API/Middlewares/ExceptionMiddleware.cs b/CleanArchitecture/CleanArchitecture.API/Middlewares/ExceptionMiddleware.cs
new file mode 100644
index 0000000..1b50462
--- /dev/null
+++ b/CleanArchitecture/CleanArchitecture.API/Middlewares/ExceptionMiddleware.cs
@@ -0,0 +1,62 @@
+using CleanArchitecture.API.Errors;
+using CleanArchitecture.Application.Exceptions;
+using System.Text.Json;
+
+namespace CleanArchitecture.API.Middlewares
+{
+ public class ExceptionMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+ private readonly IWebHostEnvironment _env;
+
+ public ExceptionMiddleware(RequestDelegate next, ILogger logger, IWebHostEnvironment env)
+ {
+ _next = next;
+ _logger = logger;
+ _env = env;
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ try
+ {
+ await _next(context);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, ex.Message);
+ context.Response.ContentType = "application/json";
+ var statusCode = StatusCodes.Status500InternalServerError;
+ var result = string.Empty;
+
+ switch (ex)
+ {
+ case NotFoundException notFoundException:
+ statusCode = StatusCodes.Status404NotFound;
+ break;
+ case ValidationException validationException:
+ statusCode = StatusCodes.Status400BadRequest;
+ var validationJson = JsonSerializer.Serialize(validationException.Errors);
+ result = JsonSerializer.Serialize(new CodeErrorException(statusCode, ex.Message, validationJson));
+ break;
+ case BadRequestException badRequestException:
+ statusCode = StatusCodes.Status400BadRequest;
+ break;
+ default:
+ break;
+ }
+
+ if(string.IsNullOrEmpty(result))
+ {
+ result = JsonSerializer.Serialize(new CodeErrorException(statusCode, ex.Message, ex.StackTrace));
+ }
+
+ context.Response.StatusCode = statusCode;
+
+ await context.Response.WriteAsync(result);
+
+ }
+ }
+ }
+}
diff --git a/CleanArchitecture/CleanArchitecture.API/Program.cs b/CleanArchitecture/CleanArchitecture.API/Program.cs
index 6e3fd1b..24c6b45 100644
--- a/CleanArchitecture/CleanArchitecture.API/Program.cs
+++ b/CleanArchitecture/CleanArchitecture.API/Program.cs
@@ -1,3 +1,8 @@
+using CleanArchitecture.Application;
+using CleanArchitecture.Infrastructure;
+using CleanArchitecture.Identity;
+using CleanArchitecture.API.Middlewares;
+
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
@@ -7,6 +12,20 @@ builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
+
+builder.Services.AddInfrastructureServices(builder.Configuration);
+builder.Services.AddApplicationServices();
+builder.Services.ConfigureIdentityServices(builder.Configuration);
+
+builder.Services.AddCors(o =>
+{
+ o.AddPolicy("CorsPolicy", builder =>
+ builder.AllowAnyOrigin().
+ AllowAnyMethod().
+ AllowAnyHeader());
+});
+
+
var app = builder.Build();
// Configure the HTTP request pipeline.
@@ -16,8 +35,13 @@ if (app.Environment.IsDevelopment())
app.UseSwaggerUI();
}
+app.UseMiddleware();
+
+app.UseAuthentication();
app.UseAuthorization();
+app.UseCors("CorsPolicy");
+
app.MapControllers();
app.Run();
diff --git a/CleanArchitecture/CleanArchitecture.API/appsettings.json b/CleanArchitecture/CleanArchitecture.API/appsettings.json
index 4d56694..7ec6057 100644
--- a/CleanArchitecture/CleanArchitecture.API/appsettings.json
+++ b/CleanArchitecture/CleanArchitecture.API/appsettings.json
@@ -1,4 +1,20 @@
{
+
+ "ConnectionStrings": {
+ "ConnectionString": "server=localhost;database=CleanArchitecture;user=root;password=securePassword",
+ "IdentityConnectionString": "server=localhost;database=CleanArchitecture.Security;user=root;password=securePassword"
+ },
+ "EmailSettings": {
+ "FromAddress": "alejandro@asarmiento.es",
+ "ApiKey": "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",
+ "FromName": "Alejandro Sarmiento"
+ },
+ "JwtSettings": {
+ "Key": "CjF*Hp$pHvsx$%wsSyfpMevUrzj@%TJv3ZjNPk34daE7N%3KjrjCnv2V76uRY8bCtH5aduTmMwdiuh%QP3iYEh$Fy*XDzz7S&pFyyZVDLDwTdFDxrP9m#A@MBgV6oNCf",
+ "Issuer": "CleanArchitectureAlejandroSarmiento",
+ "Audience": "CleanArchitectureUsers",
+ "DurationInMinutes": 360
+ },
"Logging": {
"LogLevel": {
"Default": "Information",
diff --git a/CleanArchitecture/CleanArchitecture.Application/Constants/CustomClimTypes.cs b/CleanArchitecture/CleanArchitecture.Application/Constants/CustomClimTypes.cs
new file mode 100644
index 0000000..935e117
--- /dev/null
+++ b/CleanArchitecture/CleanArchitecture.Application/Constants/CustomClimTypes.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CleanArchitecture.Application.Constants
+{
+ public static class CustomClaimTypes
+ {
+ public const string Uid = "Uid";
+ }
+}
diff --git a/CleanArchitecture/CleanArchitecture.Application/Contracts/Identity/IAuthService.cs b/CleanArchitecture/CleanArchitecture.Application/Contracts/Identity/IAuthService.cs
new file mode 100644
index 0000000..3cedfcc
--- /dev/null
+++ b/CleanArchitecture/CleanArchitecture.Application/Contracts/Identity/IAuthService.cs
@@ -0,0 +1,10 @@
+using CleanArchitecture.Application.Models.Identity;
+
+namespace CleanArchitecture.Application.Contracts.Identity
+{
+ public interface IAuthService
+ {
+ Task Login(AuthRequest request);
+ Task Register(RegistrationRequest request);
+ }
+}
diff --git a/CleanArchitecture/CleanArchitecture.Application/Exceptions/BadRequestException.cs b/CleanArchitecture/CleanArchitecture.Application/Exceptions/BadRequestException.cs
new file mode 100644
index 0000000..347362a
--- /dev/null
+++ b/CleanArchitecture/CleanArchitecture.Application/Exceptions/BadRequestException.cs
@@ -0,0 +1,11 @@
+
+namespace CleanArchitecture.Application.Exceptions
+{
+ public class BadRequestException: ApplicationException
+ {
+ public BadRequestException(string message): base(message)
+ {
+
+ }
+ }
+}
diff --git a/CleanArchitecture/CleanArchitecture.Application/Exceptions/ValidationException.cs b/CleanArchitecture/CleanArchitecture.Application/Exceptions/ValidationException.cs
index 36abe5b..132183c 100644
--- a/CleanArchitecture/CleanArchitecture.Application/Exceptions/ValidationException.cs
+++ b/CleanArchitecture/CleanArchitecture.Application/Exceptions/ValidationException.cs
@@ -8,10 +8,10 @@ namespace CleanArchitecture.Application.Exceptions
{
Errors = new Dictionary();
}
- public ValidationException(IEnumerable failures): this()
- {
- Errors = failures.GroupBy(e=>e.PropertyName, e => e.ErrorMessage).
- ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());
+ public ValidationException(IEnumerable failures): this()
+ {
+ Errors = failures.GroupBy(e=>e.PropertyName, e => e.ErrorMessage).
+ ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());
}
public IDictionary Errors { get; }
}
diff --git a/CleanArchitecture/CleanArchitecture.Application/Features/Videos/Queries/GetVideosList/GetVideosListQuery.cs b/CleanArchitecture/CleanArchitecture.Application/Features/Videos/Queries/GetVideosList/GetVideosListQuery.cs
index f896bf8..cc5083d 100644
--- a/CleanArchitecture/CleanArchitecture.Application/Features/Videos/Queries/GetVideosList/GetVideosListQuery.cs
+++ b/CleanArchitecture/CleanArchitecture.Application/Features/Videos/Queries/GetVideosList/GetVideosListQuery.cs
@@ -5,6 +5,6 @@ namespace CleanArchitecture.Application.Features.Videos.Queries.GetVideosList
public class GetVideosListQuery(string _UserName) :
IRequest>
{
- public string UserName { get; set; } = string.Empty;
+ public string UserName { get; set; } = _UserName;
}
}
diff --git a/CleanArchitecture/CleanArchitecture.Application/Features/Videos/Queries/GetVideosList/GetVideosListQueryHandler.cs b/CleanArchitecture/CleanArchitecture.Application/Features/Videos/Queries/GetVideosList/GetVideosListQueryHandler.cs
index 00b5113..dfad332 100644
--- a/CleanArchitecture/CleanArchitecture.Application/Features/Videos/Queries/GetVideosList/GetVideosListQueryHandler.cs
+++ b/CleanArchitecture/CleanArchitecture.Application/Features/Videos/Queries/GetVideosList/GetVideosListQueryHandler.cs
@@ -13,7 +13,7 @@ namespace CleanArchitecture.Application.Features.Videos.Queries.GetVideosList
public async Task> Handle(GetVideosListQuery request, CancellationToken cancellationToken)
{
- var videoList = await videoRepository.GetVideoByNombre(request.UserName);
+ var videoList = await videoRepository.GetVideoByUserName(request.UserName);
return mapper.Map>(videoList);
}
}
diff --git a/CleanArchitecture/CleanArchitecture.Application/Mappings/MappingProfile.cs b/CleanArchitecture/CleanArchitecture.Application/Mappings/MappingProfile.cs
index eaf8c91..5092811 100644
--- a/CleanArchitecture/CleanArchitecture.Application/Mappings/MappingProfile.cs
+++ b/CleanArchitecture/CleanArchitecture.Application/Mappings/MappingProfile.cs
@@ -1,5 +1,6 @@
using AutoMapper;
using CleanArchitecture.Application.Features.Streamers.Commands.CreateStreamer;
+using CleanArchitecture.Application.Features.Streamers.Commands.UpdateStreamer;
using CleanArchitecture.Application.Features.Videos.Queries.GetVideosList;
using CleanArchitecture.Domain;
@@ -11,7 +12,11 @@ namespace CleanArchitecture.Application.Mappings
public MappingProfile()
{
CreateMap