Add auto-registration mechanism #21

Merged
carltibule merged 1 commits from feature/SwallowDuplicateRegisterRequestException into develop 2023-03-03 23:42:21 -06:00
10 changed files with 177 additions and 44 deletions
Showing only changes of commit b0cb791bc2 - Show all commits

1
.gitignore vendored
View File

@ -247,3 +247,4 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
.env .env
Logs/*

View File

@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Net; using System.Net;
using YABA.API.Settings;
using YABA.API.ViewModels; using YABA.API.ViewModels;
using YABA.Service.Interfaces; using YABA.Service.Interfaces;
@ -24,7 +25,7 @@ namespace YABA.API.Controllers
} }
[HttpGet] [HttpGet]
[Obsolete] [DevOnly]
[Route("GetWebsiteMetaData")] [Route("GetWebsiteMetaData")]
[ProducesResponseType(typeof(GetWebsiteMetaDataResponse), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(GetWebsiteMetaDataResponse), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType((int)HttpStatusCode.BadRequest)]

View File

@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Mvc;
using System.Net;
using YABA.API.Settings;
namespace YABA.API.Controllers
{
[ApiController]
[ApiVersion("1")]
[DevOnly]
[Route("api/v{version:apiVersion}/[controller]")]
public class TestController : ControllerBase
{
private readonly ILogger<TestController> _logger;
public TestController(ILogger<TestController> logger)
{
_logger = logger;
}
[HttpGet("TestLog")]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public IActionResult TestLog()
{
var testObject = new { id = 1, name = "Test Message" };
_logger.LogDebug("Testing debug: {@TestObject}", testObject);
return Ok(testObject);
}
[HttpGet("TestLogError")]
[ProducesResponseType(typeof(string), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public IActionResult TestLogError()
{
var testObject = new { id = 1, name = "Test Message" };
throw new Exception();
return Ok(testObject);
}
}
}

View File

@ -24,6 +24,13 @@ namespace YABA.API.Middlewares
if (!string.IsNullOrEmpty(userAuthProviderId)) if (!string.IsNullOrEmpty(userAuthProviderId))
{ {
var userId = userService.GetUserId(userAuthProviderId); var userId = userService.GetUserId(userAuthProviderId);
if(userId <= 0)
{
var registedUser = await userService.RegisterUser(userAuthProviderId);
userId = registedUser.Id;
}
httpContext.User.Identities.FirstOrDefault().AddClaim(new Claim(ClaimsLookup.UserId.GetClaimName(), userId.ToString())); httpContext.User.Identities.FirstOrDefault().AddClaim(new Claim(ClaimsLookup.UserId.GetClaimName(), userId.ToString()));
} }
} }

View File

@ -12,6 +12,7 @@ using YABA.API.Settings.Swashbuckle;
using YABA.Data.Configuration; using YABA.Data.Configuration;
using YABA.Data.Context; using YABA.Data.Context;
using YABA.Service.Configuration; using YABA.Service.Configuration;
using Serilog;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration; var configuration = builder.Configuration;
@ -77,6 +78,10 @@ builder.Services.AddSwaggerGen(
} }
); );
// Add Serilog
Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();
builder.Host.UseSerilog();
var app = builder.Build(); var app = builder.Build();
// Run database migrations // Run database migrations

View File

@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace YABA.API.Settings
{
// Source: https://stackoverflow.com/questions/56495475/asp-net-core-its-possible-to-configure-an-action-in-controller-only-in-developm
public class DevOnlyAttribute : Attribute, IFilterFactory
{
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return new DevOnlyAttributeImpl(serviceProvider.GetRequiredService<IWebHostEnvironment>());
}
public bool IsReusable => true;
private class DevOnlyAttributeImpl : Attribute, IAuthorizationFilter
{
public DevOnlyAttributeImpl(IWebHostEnvironment hostingEnv)
{
HostingEnv = hostingEnv;
}
private IWebHostEnvironment HostingEnv { get; }
public void OnAuthorization(AuthorizationFilterContext context)
{
if (!HostingEnv.IsDevelopment())
{
context.Result = new NotFoundResult();
}
}
}
}
}

View File

@ -18,6 +18,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.1" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.1" />
<PackageReference Include="Serilog" Version="2.12.0" />
<PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.5.0" />

View File

@ -6,6 +6,18 @@
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
}, },
"Serilog": {
"MinimumLevel": "Information",
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "Logs/log-.txt",
"rollingInterval": "Day"
}
}
]
},
"WebClient": { "WebClient": {
"Url": "https://localhost:3000" "Url": "https://localhost:3000"
} }

View File

@ -1,4 +1,8 @@
using AutoMapper; using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Npgsql;
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using YABA.Common.DTOs; using YABA.Common.DTOs;
@ -13,12 +17,14 @@ namespace YABA.Service
private readonly YABAReadOnlyContext _roContext; private readonly YABAReadOnlyContext _roContext;
private readonly YABAReadWriteContext _context; private readonly YABAReadWriteContext _context;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly ILogger<UserService> _logger;
public UserService (YABAReadOnlyContext roContext, YABAReadWriteContext context, IMapper mapper) public UserService (YABAReadOnlyContext roContext, YABAReadWriteContext context, IMapper mapper, ILogger<UserService> logger)
{ {
_roContext = roContext; _roContext = roContext;
_context = context; _context = context;
_mapper = mapper; _mapper = mapper;
_logger = logger;
} }
public bool IsUserRegistered(string authProviderId) public bool IsUserRegistered(string authProviderId)
@ -28,19 +34,40 @@ namespace YABA.Service
public async Task<UserDTO> RegisterUser(string authProviderId) public async Task<UserDTO> RegisterUser(string authProviderId)
{ {
if(IsUserRegistered(authProviderId)) try
{ {
var user = _roContext.Users.FirstOrDefault(x => x.Auth0Id == authProviderId); if (!IsUserRegistered(authProviderId))
return _mapper.Map<UserDTO>(user); {
var userToRegister = new User
{
Auth0Id = authProviderId
};
var registedUser = _context.Users.Add(userToRegister);
await _context.SaveChangesAsync();
}
}
catch (Exception ex)
{
if(ex.InnerException is PostgresException &&
((PostgresException)ex.InnerException).Code == "23505")
{
var postgresException = (PostgresException)ex.InnerException;
_logger.LogWarning("Swallowing constraint violation: {@ConstraintName} for {@AuthProviderId}", postgresException.ConstraintName, authProviderId);
}
else
{
throw ex;
}
} }
var userToRegister = new User return await Get(authProviderId);
{ }
Auth0Id = authProviderId
};
var registedUser = _context.Users.Add(userToRegister); public async Task<UserDTO> Get(string authProviderId)
return await _context.SaveChangesAsync() > 0 ? _mapper.Map<UserDTO>(registedUser.Entity) : null; {
var user = await _roContext.Users.FirstOrDefaultAsync(x => x.Auth0Id == authProviderId);
return _mapper.Map<UserDTO>(user);
} }
public int GetUserId(string authProviderId) public int GetUserId(string authProviderId)

View File

@ -276,45 +276,44 @@ export function BookmarksListView(props) {
<hr className="mt-1" /> <hr className="mt-1" />
</Col> </Col>
</Row> </Row>
<Row> {
<Col xs="9" className="mb-3"> (bookmarksState.length > 0 || getSelectedBookmarksCount() > 0) && <Row>
<div className="d-flex justify-content-start align-items-center"> <Col xs="2" className="mb-3">
{ {
getDisplayedBookmarksCount() <= 0 && bookmarksState.length > 0 &&
<span className="fs-4">No bookmarks to display</span> <Button variant="primary" onClick={() => dispatchBookmarksState({type: getAreAllBookmarksSelected() ? "UNSELECT_ALL" : "SELECT_ALL"})}>{getAreAllBookmarksSelected() ? "Unselect All" : "Select All" }</Button>
} }
</Col>
{ <Col xs="7" className="mb-3">
getDisplayedBookmarksCount() > 0 &&
<Button className="me-2" variant="primary" onClick={() => dispatchBookmarksState({type: getAreAllBookmarksSelected() ? "UNSELECT_ALL" : "SELECT_ALL"})}>{getAreAllBookmarksSelected() ? "Unselect All" : "Select All" }</Button>
}
{ {
getSelectedBookmarksCount() > 0 && getSelectedBookmarksCount() > 0 &&
<DropdownButton variant="secondary" title={`${getSelectedBookmarksCount()} selected`}> <div className="d-flex justify-content-end align-items-center">
<Dropdown.Item onClick={() => handleHideBookmarks(getSelectedBookmarks().map(x => x.id))}> <span className="fs-5 me-2"> {getSelectedBookmarksCount()} selected</span>
{props.showHidden ? "Unhide" : "Hide"} <Button variant="primary" className="me-2" onClick={() => handleHideBookmarks(getSelectedBookmarks().map(x => x.id))}>{props.showHidden ? "Unhide" : "Hide"}</Button>
</Dropdown.Item> <Button
<Dropdown.Item onClick={handleDeleteMultipleBookmarks}> variant="danger"
<span className="text-danger">Delete</span> onClick={handleDeleteMultipleBookmarks}
</Dropdown.Item> >
</DropdownButton> Delete
</Button>
</div>
} }
</Col>
<Col xs="3" className="mb-3">
{
getSelectedTags().length > 0 && (
getSelectedTags().map((tag) => {
return <Button key={tag.id} variant="primary" style={{textDecoration: "none", cursor: "pointer" }} className="badge rounded-pill text-bg-primary me-2" onClick={() => onTagSelected(false, tag)}>#{tag.name} | x</Button>
})
)
}
</Col>
</Row>
}
</div>
</Col>
<Col xs="3" className="mb-3">
{
getSelectedTags().length > 0 && (
getSelectedTags().map((tag) => {
return <Button key={tag.id} variant="primary" style={{textDecoration: "none", cursor: "pointer" }} className="badge rounded-pill text-bg-primary me-2" onClick={() => onTagSelected(false, tag)}>#{tag.name} | x</Button>
})
)
}
</Col>
</Row>
<Row> <Row>
<Col xs="9"> <Col xs="9">
{ getFilteredBookmarks().length <= 0 && <div className="fs-3">No Bookmarks found</div> }
{ {
getFilteredBookmarks().map((bookmark) => { getFilteredBookmarks().map((bookmark) => {
return <Bookmark return <Bookmark
@ -329,6 +328,7 @@ export function BookmarksListView(props) {
} }
</Col> </Col>
<Col xs="3"> <Col xs="3">
{ getTagGroups(getNotSelectedTags()).length <= 0 && <div className="fs-3">No Tags gound</div> }
{ {
getTagGroups(getNotSelectedTags()).map((group) => { getTagGroups(getNotSelectedTags()).map((group) => {
return <div key={group.name} className="mb-3"> return <div key={group.name} className="mb-3">