Compare commits
17 Commits
e9d6c2ef88
...
WEBDEV-0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 8dd914c6c2 | |||
| d83b13c093 | |||
| 9fae3b978f | |||
| 822a8379aa | |||
| a5d5ed048f | |||
| baf38aa3cd | |||
| 0ad6e99a8f | |||
| 52273a8d12 | |||
| 771b63f6c4 | |||
| efb22aa0af | |||
| 067b32826d | |||
| 91c7c71299 | |||
| e240d1fdac | |||
| ba2bcb931f | |||
| 34d459cc58 | |||
| d4187b2e94 | |||
| b0cb791bc2 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -247,3 +247,5 @@ npm-debug.log*
|
|||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.env
|
.env
|
||||||
|
Logs/*
|
||||||
|
*exclude*
|
||||||
12
.woodpecker/.api_build.yaml
Normal file
12
.woodpecker/.api_build.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
when:
|
||||||
|
- event: [push, pull_request, tag]
|
||||||
|
ref: [refs/tags/APIDEV-*, refs/tags/API-*, refs/heads/api/*]
|
||||||
|
steps:
|
||||||
|
- name: (YABA.API) Build and run tests
|
||||||
|
when:
|
||||||
|
- event: [push, pull_request, tag]
|
||||||
|
branch: [api/*]
|
||||||
|
image: mcr.microsoft.com/dotnet/sdk:6.0
|
||||||
|
commands:
|
||||||
|
- dotnet build ./API
|
||||||
|
- dotnet test ./API
|
||||||
34
.woodpecker/.api_uploadimage.yaml
Normal file
34
.woodpecker/.api_uploadimage.yaml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
ref: [refs/tags/APIDEV-*, refs/tags/API-*]
|
||||||
|
steps:
|
||||||
|
- name: (YABA.API) Determining Docker image version number (dev)
|
||||||
|
when:
|
||||||
|
- ref: refs/tags/APIDEV-*
|
||||||
|
image: alpine:latest
|
||||||
|
commands:
|
||||||
|
- rm -f tags.txt
|
||||||
|
- echo ${CI_COMMIT_TAG} | sed -e "s/^APIDEV-//" >> tags.txt
|
||||||
|
- echo "latest-dev" >> tags.txt
|
||||||
|
- name: (YABA.API) Determining Docker image version number (prod)
|
||||||
|
image: alpine:latest
|
||||||
|
when:
|
||||||
|
- ref: refs/tags/API-*
|
||||||
|
commands:
|
||||||
|
- rm -f tags.txt
|
||||||
|
- echo ${CI_COMMIT_TAG} | sed -e "s/^API-//" >> tags.txt
|
||||||
|
- echo "latest" >> tags.txt
|
||||||
|
- name: (YABA.API) Package and Upload Docker Image
|
||||||
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
|
settings:
|
||||||
|
repo: gitea.iwanaga.moe/cjtibule/yaba/api
|
||||||
|
context: ./API
|
||||||
|
dockerfile: ./API/YABA.API/Dockerfile
|
||||||
|
tags_file: tags.txt
|
||||||
|
username:
|
||||||
|
from_secret: gitea_yaba_registry_username
|
||||||
|
password:
|
||||||
|
from_secret: gitea_yaba_registry_password
|
||||||
|
registry: gitea.iwanaga.moe
|
||||||
|
depends_on:
|
||||||
|
- api_build
|
||||||
52
.woodpecker/.web_uploadimage.yaml
Normal file
52
.woodpecker/.web_uploadimage.yaml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
ref: [refs/tags/WEBDEV-*, refs/tags/WEB-*]
|
||||||
|
steps:
|
||||||
|
- name: (YABA.Web) Determining Docker image version number (dev)
|
||||||
|
when:
|
||||||
|
- ref: refs/tags/WEBDEV-*
|
||||||
|
image: alpine:latest
|
||||||
|
commands:
|
||||||
|
- rm -f tags.txt
|
||||||
|
- echo ${CI_COMMIT_TAG} | sed -e "s/^WEBDEV-//" >> tags.txt
|
||||||
|
- echo "latest-dev" >> tags.txt
|
||||||
|
- name: (YABA.Web) Determining Docker image version number (prod)
|
||||||
|
when:
|
||||||
|
- ref: refs/tags/WEB-*
|
||||||
|
image: alpine:latest
|
||||||
|
commands:
|
||||||
|
- rm -f tags.txt
|
||||||
|
- echo ${CI_COMMIT_TAG} | sed -e "s/^WEB-//" >> tags.txt
|
||||||
|
- echo "latest" >> tags.txt
|
||||||
|
- name: (YABA.Web) Package and Upload Docker Image (dev)
|
||||||
|
when:
|
||||||
|
- ref: refs/tags/WEBDEV-*
|
||||||
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
|
settings:
|
||||||
|
repo: gitea.iwanaga.moe/cjtibule/yaba/web
|
||||||
|
context: ./Web
|
||||||
|
dockerfile: ./Web/Dockerfile
|
||||||
|
tags_file: tags.txt
|
||||||
|
username:
|
||||||
|
from_secret: gitea_yaba_registry_username
|
||||||
|
password:
|
||||||
|
from_secret: gitea_yaba_registry_password
|
||||||
|
registry: gitea.iwanaga.moe
|
||||||
|
build_args:
|
||||||
|
from_secret: DEV1_BUILDARGS
|
||||||
|
- name: (YABA.Web) Package and Upload Docker Image (prod)
|
||||||
|
when:
|
||||||
|
- ref: refs/tags/WEB-*
|
||||||
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
|
settings:
|
||||||
|
repo: gitea.iwanaga.moe/cjtibule/yaba/web
|
||||||
|
context: ./Web
|
||||||
|
dockerfile: ./Web/Dockerfile
|
||||||
|
tags_file: tags.txt
|
||||||
|
username:
|
||||||
|
from_secret: gitea_yaba_registry_username
|
||||||
|
password:
|
||||||
|
from_secret: gitea_yaba_registry_password
|
||||||
|
registry: gitea.iwanaga.moe
|
||||||
|
build_args:
|
||||||
|
from_secret: PROD_BUILDARGS
|
||||||
63
API/Readme.md
Normal file
63
API/Readme.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# YABA: Yet Another Bookmark App
|
||||||
|
|
||||||
|
## YABA.API - Developer Guide
|
||||||
|
|
||||||
|
### Running Migrations
|
||||||
|
When running migrations, .NET seems to be ignoring dependency injection settings. In order to get around this, be sure to add the connection string to the command like. For example, when adding the migration:
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet ef migrations add InitialMigration -p YABA.Data -s YABA.API --context YABABaseContext -- {CONNECTION_STRING_HERE}
|
||||||
|
```
|
||||||
|
|
||||||
|
When removing last migration:
|
||||||
|
```
|
||||||
|
dotnet ef migrations remove InitialMigration -p YABA.Data -s YABA.API --context YABABaseContext -- {CONNECTION_STRING_HERE}
|
||||||
|
```
|
||||||
|
|
||||||
|
When applying migrations:
|
||||||
|
```
|
||||||
|
dotnet ef database update -p YABA.Data -s YABA.API -c YABABaseContext -- { CONNECTION_STRING_HERE }
|
||||||
|
```
|
||||||
|
|
||||||
|
As per the documentation [on MSDN](https://learn.microsoft.com/en-ca/ef/core/cli/dbcontext-creation?tabs=dotnet-core-cli#from-application-services):
|
||||||
|
> The -- token directs dotnet ef to treat everything that follows as an argument and not try to parse them as options. Any extra arguments not used by dotnet ef are forwarded to the app.
|
||||||
|
|
||||||
|
### Managing secrets
|
||||||
|
|
||||||
|
Best practice dictates that sensitive values should never be committed to source control. To manage secrets locally, this project utilize the [secrets manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-7.0&tabs=windows).
|
||||||
|
|
||||||
|
To initialize a secrets manager:
|
||||||
|
```
|
||||||
|
dotnet user-secrets init --project YABA.API
|
||||||
|
```
|
||||||
|
|
||||||
|
To set (or override) the value of a secret using a provided key:
|
||||||
|
```
|
||||||
|
dotnet user-secrets set "Key" "Value" --project YABA.API
|
||||||
|
```
|
||||||
|
```
|
||||||
|
dotnet user-secrets set "Object:Property" "Value" --project YABA.API
|
||||||
|
```
|
||||||
|
|
||||||
|
To list all secets:
|
||||||
|
```
|
||||||
|
dotnet user-secrets list --project YABA.API
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
To build a dockerized version of the API project:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build { API_PROJECT_ROOT_SOURCE } -f { API_PROJECT_DOCKERFILE_PATH } -t { imagename:tag }
|
||||||
|
```
|
||||||
|
|
||||||
|
In order to run a container using the image built above, keep the following things in mind:
|
||||||
|
- It might be necessary to map container port 80 to another
|
||||||
|
- In the absence of a linked user secrets, secrets will have to be passed in to the container as environment variables, prefixed with `ASPNETCORE_`
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -d -p { HOST_PC_PORT_HERE }:80 --env ASPNETCORE_Object__Property=Value --name { CONTAINER_NAME } {imagename:tag}
|
||||||
|
```
|
||||||
|
|
||||||
|
Environment variables that are explicitly listed in `YABA.API\Dockerfile` will have to be properly set for proper operation of the application
|
||||||
@ -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)]
|
||||||
42
API/YABA.API/Controllers/TestController.cs
Normal file
42
API/YABA.API/Controllers/TestController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
API/YABA.API/Dockerfile
Normal file
38
API/YABA.API/Dockerfile
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS base
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 80
|
||||||
|
EXPOSE 443
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV ASPNETCORE_Environment=
|
||||||
|
ENV ASPNETCORE_Authentication__Auth0__ClientId=
|
||||||
|
ENV ASPNETCORE_Authentication__Auth0__ClientSecret=
|
||||||
|
ENV ASPNETCORE_Authentication__Auth0__Domain=
|
||||||
|
ENV ASPNETCORE_Authentication__Auth0__Identifier=
|
||||||
|
ENV ASPNETCORE_ConnectionStrings__YABAReadOnlyDbConnectionString=
|
||||||
|
ENV ASPNETCORE_ConnectionStrings__YABAReadWriteDbConnectionString=
|
||||||
|
ENV WebClient__Url=
|
||||||
|
ENV Serilog__WriteTo__1__Args__Uri=
|
||||||
|
ENV Serilog__WriteTo__1__Args__Labels__1__Value=
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["YABA.API/YABA.API.csproj", "YABA.API/"]
|
||||||
|
COPY ["YABA.Common/YABA.Common.csproj", "YABA.Common/"]
|
||||||
|
COPY ["YABA.Data/YABA.Data.csproj", "YABA.Data/"]
|
||||||
|
COPY ["YABA.Models/YABA.Models.csproj", "YABA.Models/"]
|
||||||
|
COPY ["YABA.Service/YABA.Service.csproj", "YABA.Service/"]
|
||||||
|
RUN dotnet restore "YABA.API/YABA.API.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/YABA.API"
|
||||||
|
RUN dotnet build "YABA.API.csproj" -c Release -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
RUN dotnet publish "YABA.API.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "YABA.API.dll"]
|
||||||
@ -2,6 +2,7 @@
|
|||||||
using YABA.API.Extensions;
|
using YABA.API.Extensions;
|
||||||
using YABA.Common.Extensions;
|
using YABA.Common.Extensions;
|
||||||
using YABA.Common.Lookups;
|
using YABA.Common.Lookups;
|
||||||
|
using YABA.Common.Utils;
|
||||||
using YABA.Service.Interfaces;
|
using YABA.Service.Interfaces;
|
||||||
|
|
||||||
namespace YABA.API.Middlewares
|
namespace YABA.API.Middlewares
|
||||||
@ -24,7 +25,14 @@ namespace YABA.API.Middlewares
|
|||||||
if (!string.IsNullOrEmpty(userAuthProviderId))
|
if (!string.IsNullOrEmpty(userAuthProviderId))
|
||||||
{
|
{
|
||||||
var userId = userService.GetUserId(userAuthProviderId);
|
var userId = userService.GetUserId(userAuthProviderId);
|
||||||
httpContext.User.Identities.FirstOrDefault().AddClaim(new Claim(ClaimsLookup.UserId.GetClaimName(), userId.ToString()));
|
|
||||||
|
if(userId <= 0)
|
||||||
|
{
|
||||||
|
var registedUser = await userService.RegisterUser(userAuthProviderId);
|
||||||
|
userId = registedUser.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpContext.User.Identities.FirstOrDefault().AddClaim(new Claim(ClaimsLookup.UserId.GetClaimName(EnvironmentUtils.IsDevelopmentEnvironment()), userId.ToString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
using Serilog.Context;
|
||||||
|
using YABA.Common.Extensions;
|
||||||
|
|
||||||
|
namespace YABA.API.Middlewares
|
||||||
|
{
|
||||||
|
public class AddCustomLoggingPropertiesMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
|
||||||
|
public AddCustomLoggingPropertiesMiddleware(RequestDelegate next)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
if(httpContext.Request.Path.HasValue && httpContext.Request.Path.Value.Contains("/api"))
|
||||||
|
{
|
||||||
|
LogContext.PushProperty("UserId", httpContext.User.Identity.IsAuthenticated ? httpContext.User.Identity.GetUserId() : "Anonymous");
|
||||||
|
LogContext.PushProperty("RemoteIpAddress", httpContext.Connection.RemoteIpAddress.MapToIPv4());
|
||||||
|
}
|
||||||
|
|
||||||
|
await _next(httpContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,8 +12,12 @@ 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;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddEnvironmentVariables();
|
||||||
|
|
||||||
var configuration = builder.Configuration;
|
var configuration = builder.Configuration;
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
@ -48,6 +52,7 @@ builder.Services.AddHttpContextAccessor();
|
|||||||
builder.Services.AddServiceProjectDependencyInjectionConfiguration(configuration);
|
builder.Services.AddServiceProjectDependencyInjectionConfiguration(configuration);
|
||||||
builder.Services.AddDataProjectDependencyInjectionConfiguration(configuration);
|
builder.Services.AddDataProjectDependencyInjectionConfiguration(configuration);
|
||||||
builder.Services.AddControllers().AddNewtonsoftJson();
|
builder.Services.AddControllers().AddNewtonsoftJson();
|
||||||
|
builder.Services.AddHealthChecks();
|
||||||
|
|
||||||
// Add AutoMapper profiles
|
// Add AutoMapper profiles
|
||||||
var mapperConfiguration = new MapperConfiguration(mapperConfiguration =>
|
var mapperConfiguration = new MapperConfiguration(mapperConfiguration =>
|
||||||
@ -77,6 +82,16 @@ builder.Services.AddSwaggerGen(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||||
|
{
|
||||||
|
options.ForwardedHeaders =
|
||||||
|
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add Serilog
|
||||||
|
Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).Enrich.FromLogContext().CreateLogger();
|
||||||
|
builder.Host.UseSerilog();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Run database migrations
|
// Run database migrations
|
||||||
@ -95,6 +110,7 @@ if (app.Environment.IsDevelopment())
|
|||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseForwardedHeaders();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
@ -102,13 +118,10 @@ app.MapControllers();
|
|||||||
|
|
||||||
// Add custom middlewares
|
// Add custom middlewares
|
||||||
app.UseMiddleware<AddCustomClaimsMiddleware>();
|
app.UseMiddleware<AddCustomClaimsMiddleware>();
|
||||||
|
app.UseMiddleware<AddCustomLoggingPropertiesMiddleware>();
|
||||||
app.UseCors(x => x
|
|
||||||
.AllowAnyOrigin()
|
|
||||||
.AllowAnyMethod()
|
|
||||||
.AllowAnyHeader());
|
|
||||||
|
|
||||||
var webClientUrl = configuration.GetSection("WebClient").GetValue<string>("Url");
|
var webClientUrl = configuration.GetSection("WebClient").GetValue<string>("Url");
|
||||||
app.UseCors(x => x.AllowAnyHeader().AllowAnyMethod().WithOrigins(webClientUrl));
|
app.UseCors(x => x.WithOrigins(webClientUrl).AllowAnyMethod().AllowAnyHeader());
|
||||||
|
app.MapHealthChecks("/Pulse");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
34
API/YABA.API/Settings/DevOnlyAttribute.cs
Normal file
34
API/YABA.API/Settings/DevOnlyAttribute.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,13 +11,22 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.13" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.13" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.14" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.14" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.17">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.14">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<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.Enrichers.Environment" Version="2.2.0" />
|
||||||
|
<PackageReference Include="Serilog.Enrichers.Process" Version="2.0.2" />
|
||||||
|
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||||
|
<PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.1.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" />
|
||||||
@ -29,4 +38,8 @@
|
|||||||
<ProjectReference Include="..\YABA.Service\YABA.Service.csproj" />
|
<ProjectReference Include="..\YABA.Service\YABA.Service.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Logs\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
46
API/YABA.API/appsettings.json
Normal file
46
API/YABA.API/appsettings.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft": "Warning",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Serilog": {
|
||||||
|
"MinimumLevel": "Information",
|
||||||
|
"Using": [
|
||||||
|
"Serilog.Sinks.Grafana.Loki"
|
||||||
|
],
|
||||||
|
"WriteTo": [
|
||||||
|
{
|
||||||
|
"Name": "File",
|
||||||
|
"Args": {
|
||||||
|
"path": "Logs/log.json",
|
||||||
|
"rollingInterval": "Day",
|
||||||
|
"formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "GrafanaLoki",
|
||||||
|
"Args": {
|
||||||
|
"uri": "https://loki.iwanaga.moe",
|
||||||
|
"labels": [
|
||||||
|
{
|
||||||
|
"key": "job",
|
||||||
|
"value": "YABA.API"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "environment",
|
||||||
|
"value": "localhost"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"propertiesAsLabels": [ "job", "environment" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Enrich": ["FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId"]
|
||||||
|
},
|
||||||
|
"WebClient": {
|
||||||
|
"Url": "http://localhost:3000"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
API/YABA.Common/Attributes/ClaimNameAttribute.cs
Normal file
16
API/YABA.Common/Attributes/ClaimNameAttribute.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace YABA.Common.Attributes
|
||||||
|
{
|
||||||
|
public class ClaimNameAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string DevClaimName { get; private set; }
|
||||||
|
public string ProdClaimName { get; private set; }
|
||||||
|
|
||||||
|
public ClaimNameAttribute(string devClaimName, string prodClaimName)
|
||||||
|
{
|
||||||
|
this.DevClaimName = devClaimName;
|
||||||
|
ProdClaimName = prodClaimName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,9 +28,9 @@ namespace YABA.Common.Extensions
|
|||||||
return enumValue.GetAttribute<DisplayAttribute>().Name;
|
return enumValue.GetAttribute<DisplayAttribute>().Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetClaimName(this ClaimsLookup claimLookup)
|
public static string GetClaimName(this ClaimsLookup claimLookup, bool isDevelopmentEnvironment)
|
||||||
{
|
{
|
||||||
return claimLookup.GetAttribute<ClaimNameAttribute>().Name;
|
return isDevelopmentEnvironment ? claimLookup.GetAttribute<ClaimNameAttribute>().DevClaimName : claimLookup.GetAttribute<ClaimNameAttribute>().ProdClaimName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsCrudResultSuccessful(this CrudResultLookup importStatusLookup) => SuccessfulCrudStatuses.Contains(importStatusLookup);
|
public static bool IsCrudResultSuccessful(this CrudResultLookup importStatusLookup) => SuccessfulCrudStatuses.Contains(importStatusLookup);
|
||||||
@ -1,6 +1,8 @@
|
|||||||
using System.Security.Claims;
|
using System;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Security.Principal;
|
using System.Security.Principal;
|
||||||
using YABA.Common.Lookups;
|
using YABA.Common.Lookups;
|
||||||
|
using YABA.Common.Utils;
|
||||||
|
|
||||||
namespace YABA.Common.Extensions
|
namespace YABA.Common.Extensions
|
||||||
{
|
{
|
||||||
@ -12,7 +14,7 @@ namespace YABA.Common.Extensions
|
|||||||
public static string GetCustomClaim(this IIdentity identity, ClaimsLookup claim)
|
public static string GetCustomClaim(this IIdentity identity, ClaimsLookup claim)
|
||||||
{
|
{
|
||||||
var claimsIdentity = identity as ClaimsIdentity;
|
var claimsIdentity = identity as ClaimsIdentity;
|
||||||
return claimsIdentity.FindFirst(claim.GetClaimName())?.Value.ToString();
|
return claimsIdentity.FindFirst(claim.GetClaimName(EnvironmentUtils.IsDevelopmentEnvironment()))?.Value.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
22
API/YABA.Common/Lookups/ClaimsLookup.cs
Normal file
22
API/YABA.Common/Lookups/ClaimsLookup.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using YABA.Common.Attributes;
|
||||||
|
|
||||||
|
namespace YABA.Common.Lookups
|
||||||
|
{
|
||||||
|
public enum ClaimsLookup
|
||||||
|
{
|
||||||
|
[ClaimName("https://auth.dev.iwanaga.moe/api/auth_provider_id", "https://auth.iwanaga.moe/api/auth_provider_id")]
|
||||||
|
AuthProviderId = 1,
|
||||||
|
|
||||||
|
[ClaimName("https://auth.dev.iwanaga.moe/api/email_address", "https://auth.iwanaga.moe/api/email_address")]
|
||||||
|
UserEmail = 2,
|
||||||
|
|
||||||
|
[ClaimName("https://auth.dev.iwanaga.moe/api/email_verified", "https://auth.iwanaga.moe/api/email_verified")]
|
||||||
|
IsEmailConfirmed = 3,
|
||||||
|
|
||||||
|
[ClaimName("https://auth.dev.iwanaga.moe/api/username", "https://auth.iwanaga.moe/api/username")]
|
||||||
|
Username = 4,
|
||||||
|
|
||||||
|
[ClaimName("https://auth.dev.iwanaga.moe/api/id", "https://auth.iwanaga.moe/api/id")]
|
||||||
|
UserId = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
9
API/YABA.Common/Utils/EnvironmentUtils.cs
Normal file
9
API/YABA.Common/Utils/EnvironmentUtils.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace YABA.Common.Utils
|
||||||
|
{
|
||||||
|
public static class EnvironmentUtils
|
||||||
|
{
|
||||||
|
public static bool IsDevelopmentEnvironment() => Environment.GetEnvironmentVariable("ASPNETCORE_Environment") == "Development";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.1</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
@ -12,6 +12,7 @@ namespace YABA.Data.Context
|
|||||||
{
|
{
|
||||||
public class YABABaseContext : DbContext
|
public class YABABaseContext : DbContext
|
||||||
{
|
{
|
||||||
|
public YABABaseContext() : base() { }
|
||||||
public YABABaseContext(DbContextOptions<YABABaseContext> options) : base(options) { }
|
public YABABaseContext(DbContextOptions<YABABaseContext> options) : base(options) { }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
@ -39,14 +40,18 @@ namespace YABA.Data.Context
|
|||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
modelBuilder.Entity<Tag>()
|
modelBuilder.Entity<Tag>()
|
||||||
.HasIndex(x => x.Name)
|
.HasIndex(x => new { x.Name, x.UserId })
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
modelBuilder.Entity<Bookmark>()
|
||||||
|
.HasIndex(x => new { x.Url, x.UserId })
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DbSet<Bookmark> Bookmarks { get; set; }
|
public virtual DbSet<Bookmark> Bookmarks { get; set; }
|
||||||
public DbSet<Tag> Tags { get; set; }
|
public virtual DbSet<Tag> Tags { get; set; }
|
||||||
public DbSet<BookmarkTag> BookmarkTags { get; set; }
|
public virtual DbSet<BookmarkTag> BookmarkTags { get; set; }
|
||||||
public DbSet<User> Users { get; set; }
|
public virtual DbSet<User> Users { get; set; }
|
||||||
|
|
||||||
private static LambdaExpression ConvertFilterExpression<TInterface>(
|
private static LambdaExpression ConvertFilterExpression<TInterface>(
|
||||||
Expression<Func<TInterface, bool>> filterExpression,
|
Expression<Func<TInterface, bool>> filterExpression,
|
||||||
@ -4,6 +4,7 @@ namespace YABA.Data.Context
|
|||||||
{
|
{
|
||||||
public class YABAReadOnlyContext : YABABaseContext
|
public class YABAReadOnlyContext : YABABaseContext
|
||||||
{
|
{
|
||||||
|
public YABAReadOnlyContext() : base() { }
|
||||||
public YABAReadOnlyContext(DbContextOptions<YABABaseContext> options) : base(options)
|
public YABAReadOnlyContext(DbContextOptions<YABABaseContext> options) : base(options)
|
||||||
{
|
{
|
||||||
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||||
@ -9,6 +9,8 @@ namespace YABA.Data.Context
|
|||||||
{
|
{
|
||||||
public class YABAReadWriteContext : YABABaseContext
|
public class YABAReadWriteContext : YABABaseContext
|
||||||
{
|
{
|
||||||
|
public YABAReadWriteContext() : base() { }
|
||||||
|
|
||||||
public YABAReadWriteContext(DbContextOptions<YABABaseContext> options) : base(options)
|
public YABAReadWriteContext(DbContextOptions<YABABaseContext> options) : base(options)
|
||||||
{
|
{
|
||||||
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
|
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
|
||||||
@ -0,0 +1,214 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using YABA.Data.Context;
|
||||||
|
|
||||||
|
namespace YABA.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(YABABaseContext))]
|
||||||
|
[Migration("20230304061552_AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark")]
|
||||||
|
partial class AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63)
|
||||||
|
.HasAnnotation("ProductVersion", "5.0.17")
|
||||||
|
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||||
|
|
||||||
|
modelBuilder.Entity("YABA.Models.Bookmark", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedOn")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_on");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("description");
|
||||||
|
|
||||||
|
b.Property<bool>("IsHidden")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_hidden");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("LastModified")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_modified");
|
||||||
|
|
||||||
|
b.Property<string>("Note")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("note");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("title");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("url");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_bookmarks");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("ix_bookmarks_user_id");
|
||||||
|
|
||||||
|
b.HasIndex("Url", "UserId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_bookmarks_url_user_id");
|
||||||
|
|
||||||
|
b.ToTable("bookmarks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("BookmarkId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("bookmark_id");
|
||||||
|
|
||||||
|
b.Property<int>("TagId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("tag_id");
|
||||||
|
|
||||||
|
b.HasKey("BookmarkId", "TagId")
|
||||||
|
.HasName("pk_bookmark_tags");
|
||||||
|
|
||||||
|
b.HasIndex("TagId")
|
||||||
|
.HasDatabaseName("ix_bookmark_tags_tag_id");
|
||||||
|
|
||||||
|
b.ToTable("bookmark_tags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("YABA.Models.Tag", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||||
|
|
||||||
|
b.Property<bool>("IsHidden")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_hidden");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user_id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_tags");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("ix_tags_user_id");
|
||||||
|
|
||||||
|
b.HasIndex("Name", "UserId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_tags_name_user_id");
|
||||||
|
|
||||||
|
b.ToTable("tags");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("YABA.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||||
|
|
||||||
|
b.Property<string>("Auth0Id")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("auth0id");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedOn")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_on");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_deleted");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("LastModified")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_modified");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_users");
|
||||||
|
|
||||||
|
b.HasIndex("Auth0Id")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_users_auth0id");
|
||||||
|
|
||||||
|
b.ToTable("users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("YABA.Models.Bookmark", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("YABA.Models.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.HasConstraintName("fk_bookmarks_users_user_id")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("YABA.Models.BookmarkTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("YABA.Models.Bookmark", "Bookmark")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BookmarkId")
|
||||||
|
.HasConstraintName("fk_bookmark_tags_bookmarks_bookmark_id")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("YABA.Models.Tag", "Tag")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TagId")
|
||||||
|
.HasConstraintName("fk_bookmark_tags_tags_tag_id")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Bookmark");
|
||||||
|
|
||||||
|
b.Navigation("Tag");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("YABA.Models.Tag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("YABA.Models.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.HasConstraintName("fk_tags_users_user_id")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace YABA.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddUniqueNameAndUserIdComboConstraintToTagsTable_AddUniqueUrlAndUserIdComboConstraintToBookmark : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_tags_name",
|
||||||
|
table: "tags");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_tags_name_user_id",
|
||||||
|
table: "tags",
|
||||||
|
columns: new[] { "name", "user_id" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_bookmarks_url_user_id",
|
||||||
|
table: "bookmarks",
|
||||||
|
columns: new[] { "url", "user_id" },
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_tags_name_user_id",
|
||||||
|
table: "tags");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_bookmarks_url_user_id",
|
||||||
|
table: "bookmarks");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_tags_name",
|
||||||
|
table: "tags",
|
||||||
|
column: "name",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -67,6 +67,10 @@ namespace YABA.Data.Migrations
|
|||||||
b.HasIndex("UserId")
|
b.HasIndex("UserId")
|
||||||
.HasDatabaseName("ix_bookmarks_user_id");
|
.HasDatabaseName("ix_bookmarks_user_id");
|
||||||
|
|
||||||
|
b.HasIndex("Url", "UserId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_bookmarks_url_user_id");
|
||||||
|
|
||||||
b.ToTable("bookmarks");
|
b.ToTable("bookmarks");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -113,13 +117,13 @@ namespace YABA.Data.Migrations
|
|||||||
b.HasKey("Id")
|
b.HasKey("Id")
|
||||||
.HasName("pk_tags");
|
.HasName("pk_tags");
|
||||||
|
|
||||||
b.HasIndex("Name")
|
|
||||||
.IsUnique()
|
|
||||||
.HasDatabaseName("ix_tags_name");
|
|
||||||
|
|
||||||
b.HasIndex("UserId")
|
b.HasIndex("UserId")
|
||||||
.HasDatabaseName("ix_tags_user_id");
|
.HasDatabaseName("ix_tags_user_id");
|
||||||
|
|
||||||
|
b.HasIndex("Name", "UserId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_tags_name_user_id");
|
||||||
|
|
||||||
b.ToTable("tags");
|
b.ToTable("tags");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1,20 +1,20 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.1</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="EFCore.NamingConventions" Version="5.0.2" />
|
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.17" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.14" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.17">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.14">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.10" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.8" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using YABA.Common.Interfaces;
|
using YABA.Common.Interfaces;
|
||||||
@ -21,5 +22,7 @@ namespace YABA.Models
|
|||||||
[ForeignKey(nameof(User))]
|
[ForeignKey(nameof(User))]
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
public virtual User User { get; set; }
|
public virtual User User { get; set; }
|
||||||
|
|
||||||
|
public ICollection<BookmarkTag> BookmarkTags { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using YABA.Models.Interfaces;
|
using YABA.Models.Interfaces;
|
||||||
|
|
||||||
@ -14,5 +15,7 @@ namespace YABA.Models
|
|||||||
[ForeignKey(nameof(User))]
|
[ForeignKey(nameof(User))]
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
public virtual User User { get; set; }
|
public virtual User User { get; set; }
|
||||||
|
|
||||||
|
public virtual ICollection<BookmarkTag> TagBookmarks { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.1</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moq;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Xunit;
|
||||||
|
using YABA.Data.Context;
|
||||||
|
using YABA.Models;
|
||||||
|
|
||||||
|
namespace YABA.Service.Tests
|
||||||
|
{
|
||||||
|
public partial class UserServiceTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[ClassData(typeof(IsUserRegistedTestData))]
|
||||||
|
public void IsUserRegistedTests(IsUserRegisteredScenario scenario)
|
||||||
|
{
|
||||||
|
var usersDbSet = new Mock<DbSet<User>>();
|
||||||
|
usersDbSet.As<IQueryable<User>>().Setup(m => m.Provider).Returns(scenario.Users.Provider);
|
||||||
|
usersDbSet.As<IQueryable<User>>().Setup(m => m.Expression).Returns(scenario.Users.Expression);
|
||||||
|
usersDbSet.As<IQueryable<User>>().Setup(m => m.ElementType).Returns(scenario.Users.ElementType);
|
||||||
|
usersDbSet.As<IQueryable<User>>().Setup(m => m.GetEnumerator()).Returns(() => scenario.Users.GetEnumerator());
|
||||||
|
|
||||||
|
var mockContext = new Mock<YABAReadOnlyContext>();
|
||||||
|
mockContext.Setup(x => x.Users).Returns(usersDbSet.Object);
|
||||||
|
|
||||||
|
var userService = new UserService(mockContext.Object, null, null, null);
|
||||||
|
var actualIsUserRegistered = userService.IsUserRegistered(scenario.AuthProviderId);
|
||||||
|
Assert.Equal(scenario.ExpectedResult, actualIsUserRegistered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IsUserRegistedTestData : TheoryData<IsUserRegisteredScenario>
|
||||||
|
{
|
||||||
|
public IsUserRegistedTestData()
|
||||||
|
{
|
||||||
|
Add(new IsUserRegisteredScenario
|
||||||
|
{
|
||||||
|
Users = new List<User>
|
||||||
|
{
|
||||||
|
new User { Id = 1, Auth0Id = "auth0|TestId1" },
|
||||||
|
}.AsQueryable(),
|
||||||
|
AuthProviderId = "auth0|TestId1",
|
||||||
|
ExpectedResult = true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Not Found
|
||||||
|
Add(new IsUserRegisteredScenario
|
||||||
|
{
|
||||||
|
Users = new List<User>
|
||||||
|
{
|
||||||
|
new User { Id = 1, Auth0Id = "auth0|TestId1" },
|
||||||
|
}.AsQueryable(),
|
||||||
|
AuthProviderId = "auth0|TestId2",
|
||||||
|
ExpectedResult = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IsUserRegisteredScenario
|
||||||
|
{
|
||||||
|
public IQueryable<User> Users { get; set; }
|
||||||
|
public string AuthProviderId { get; set; }
|
||||||
|
public bool ExpectedResult { get; set; }
|
||||||
|
public bool ActualResult { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
27
API/YABA.Service.Tests/YABA.Service.Tests.csproj
Normal file
27
API/YABA.Service.Tests/YABA.Service.Tests.csproj
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||||
|
<PackageReference Include="Moq" Version="4.18.4" />
|
||||||
|
<PackageReference Include="xunit" Version="2.4.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\YABA.Service\YABA.Service.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@ -2,6 +2,7 @@
|
|||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@ -40,26 +41,28 @@ namespace YABA.Service
|
|||||||
public IEnumerable<BookmarkDTO> GetAll(bool showHidden = false)
|
public IEnumerable<BookmarkDTO> GetAll(bool showHidden = false)
|
||||||
{
|
{
|
||||||
var currentUserId = GetCurrentUserId();
|
var currentUserId = GetCurrentUserId();
|
||||||
|
|
||||||
var bookmarkTagsLookup = _roContext.BookmarkTags
|
|
||||||
.Include(x => x.Bookmark)
|
|
||||||
.Include(x => x.Tag)
|
|
||||||
.Where(x => x.Bookmark.UserId == currentUserId && x.Tag.UserId == currentUserId)
|
|
||||||
.ToList()
|
|
||||||
.GroupBy(x => x.BookmarkId)
|
|
||||||
.ToDictionary(k => k.Key, v => new KeyValuePair<Bookmark, List<Tag>>(v.FirstOrDefault()?.Bookmark, v.Select(x => x.Tag).ToList()));
|
|
||||||
var userBookmarkDTOs = new List<BookmarkDTO>();
|
var userBookmarkDTOs = new List<BookmarkDTO>();
|
||||||
|
|
||||||
foreach (var bookmarkTag in bookmarkTagsLookup)
|
|
||||||
|
var showHiddenCondition = new Func<Bookmark, bool>(b => b.IsHidden || b.BookmarkTags.Where(x => x.Tag.UserId == currentUserId).Any(x => x.Tag.IsHidden));
|
||||||
|
var showNotHiddenCondition = new Func<Bookmark, bool>(b => !b.IsHidden && b.BookmarkTags.Where(x => x.Tag.UserId == currentUserId).All(bt => !bt.Tag.IsHidden));
|
||||||
|
|
||||||
|
var filteredBookmarks = _roContext.Bookmarks
|
||||||
|
.Include(b => b.BookmarkTags)
|
||||||
|
.ThenInclude(bt => bt.Tag)
|
||||||
|
.Where(b => b.UserId == currentUserId)
|
||||||
|
.Where(showHidden ? showHiddenCondition : showNotHiddenCondition)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach(var bookmark in filteredBookmarks)
|
||||||
{
|
{
|
||||||
if ((showHidden && (bookmarkTag.Value.Key.IsHidden || bookmarkTag.Value.Value.Any(x => x.IsHidden))) // If showHidden = true, only add Bookmarks that are marked Hidden OR when any of its Tags are marked Hidden
|
var isBookmarkHidden = bookmark.IsHidden;
|
||||||
|| (!showHidden && !bookmarkTag.Value.Key.IsHidden && bookmarkTag.Value.Value.All(x => !x.IsHidden))) // If showHidden = false, only add when Bookmark is not marked Hidden AND when any of its Tags are not marked as Hidden
|
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark);
|
||||||
{
|
|
||||||
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmarkTag.Value.Key);
|
var bookmarkTags = bookmark.BookmarkTags.Select(x => x.Tag);
|
||||||
var bookmarkTagDTOs = _mapper.Map<IEnumerable<TagDTO>>(bookmarkTag.Value.Value);
|
bookmarkDTO.Tags = _mapper.Map<List<TagDTO>>(bookmarkTags);
|
||||||
bookmarkDTO.Tags.AddRange(bookmarkTagDTOs);
|
|
||||||
userBookmarkDTOs.Add(bookmarkDTO);
|
userBookmarkDTOs.Add(bookmarkDTO);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return userBookmarkDTOs;
|
return userBookmarkDTOs;
|
||||||
@ -110,14 +113,10 @@ namespace YABA.Service
|
|||||||
bookmark.Url = !string.IsNullOrEmpty(request.Url) ? request.Url : bookmark.Url;
|
bookmark.Url = !string.IsNullOrEmpty(request.Url) ? request.Url : bookmark.Url;
|
||||||
UpdateBookmarkWithMetaData(bookmark);
|
UpdateBookmarkWithMetaData(bookmark);
|
||||||
|
|
||||||
if (await _context.SaveChangesAsync() > 0)
|
await _context.SaveChangesAsync();
|
||||||
{
|
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark);
|
||||||
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark);
|
bookmarkDTO.Tags = tags;
|
||||||
bookmarkDTO.Tags = tags;
|
return bookmarkDTO;
|
||||||
return bookmarkDTO;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<TagDTO>?> UpdateBookmarkTags(int id, IEnumerable<string> tags)
|
public async Task<IEnumerable<TagDTO>?> UpdateBookmarkTags(int id, IEnumerable<string> tags)
|
||||||
@ -253,6 +252,7 @@ namespace YABA.Service
|
|||||||
private void UpdateBookmarkWithMetaData(IBookmark bookmark)
|
private void UpdateBookmarkWithMetaData(IBookmark bookmark)
|
||||||
{
|
{
|
||||||
var webClient = new WebClient();
|
var webClient = new WebClient();
|
||||||
|
webClient.Headers.Add("User-Agent", "APIClient");
|
||||||
var sourceData = webClient.DownloadString(bookmark.Url);
|
var sourceData = webClient.DownloadString(bookmark.Url);
|
||||||
var title = Regex.Match(sourceData, @"\<title\b[^>]*\>\s*(?<Title>[\s\S]*?)\</title\>", RegexOptions.IgnoreCase).Groups["Title"].Value;
|
var title = Regex.Match(sourceData, @"\<title\b[^>]*\>\s*(?<Title>[\s\S]*?)\</title\>", RegexOptions.IgnoreCase).Groups["Title"].Value;
|
||||||
var description = string.Empty;
|
var description = string.Empty;
|
||||||
79
API/YABA.Service/UserService.cs
Normal file
79
API/YABA.Service/UserService.cs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Npgsql;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using YABA.Common.DTOs;
|
||||||
|
using YABA.Data.Context;
|
||||||
|
using YABA.Models;
|
||||||
|
using YABA.Service.Interfaces;
|
||||||
|
|
||||||
|
namespace YABA.Service
|
||||||
|
{
|
||||||
|
public class UserService : IUserService
|
||||||
|
{
|
||||||
|
private readonly YABAReadOnlyContext _roContext;
|
||||||
|
private readonly YABAReadWriteContext _context;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly ILogger<UserService> _logger;
|
||||||
|
|
||||||
|
public UserService (YABAReadOnlyContext roContext, YABAReadWriteContext context, IMapper mapper, ILogger<UserService> logger)
|
||||||
|
{
|
||||||
|
_roContext = roContext;
|
||||||
|
_context = context;
|
||||||
|
_mapper = mapper;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsUserRegistered(string authProviderId)
|
||||||
|
{
|
||||||
|
return _roContext.Users.Any(x => x.Auth0Id == authProviderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserDTO> RegisterUser(string authProviderId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!IsUserRegistered(authProviderId))
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Get(authProviderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserDTO> Get(string authProviderId)
|
||||||
|
{
|
||||||
|
var user = await _roContext.Users.FirstOrDefaultAsync(x => x.Auth0Id == authProviderId);
|
||||||
|
return _mapper.Map<UserDTO>(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetUserId(string authProviderId)
|
||||||
|
{
|
||||||
|
var user = _roContext.Users.FirstOrDefault(x => x.Auth0Id == authProviderId);
|
||||||
|
return user != null ? user.Id : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.1</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
@ -11,7 +11,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.Data", "YABA.Data\YABA
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.Service", "YABA.Service\YABA.Service.csproj", "{0098D0A8-0273-46F1-9FE7-B0409442251A}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.Service", "YABA.Service\YABA.Service.csproj", "{0098D0A8-0273-46F1-9FE7-B0409442251A}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YABA.Common", "YABA.Common\YABA.Common.csproj", "{CA107B5D-4B8E-4515-8380-CB474C57F79C}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.Common", "YABA.Common\YABA.Common.csproj", "{CA107B5D-4B8E-4515-8380-CB474C57F79C}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{17A95D6E-D7F8-40F0-A140-1556492495DB}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CA49C56-9635-4BA7-A88A-DA16264E2ED2}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YABA.Service.Tests", "YABA.Service.Tests\YABA.Service.Tests.csproj", "{00E8AC4F-B7B3-4DC5-8BA1-BB7DDC4A9071}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@ -39,10 +45,22 @@ Global
|
|||||||
{CA107B5D-4B8E-4515-8380-CB474C57F79C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{CA107B5D-4B8E-4515-8380-CB474C57F79C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{CA107B5D-4B8E-4515-8380-CB474C57F79C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{CA107B5D-4B8E-4515-8380-CB474C57F79C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{CA107B5D-4B8E-4515-8380-CB474C57F79C}.Release|Any CPU.Build.0 = Release|Any CPU
|
{CA107B5D-4B8E-4515-8380-CB474C57F79C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{00E8AC4F-B7B3-4DC5-8BA1-BB7DDC4A9071}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{00E8AC4F-B7B3-4DC5-8BA1-BB7DDC4A9071}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{00E8AC4F-B7B3-4DC5-8BA1-BB7DDC4A9071}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{00E8AC4F-B7B3-4DC5-8BA1-BB7DDC4A9071}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{97FC2A4F-B663-43B0-987D-DC3EA7ADD66C} = {17A95D6E-D7F8-40F0-A140-1556492495DB}
|
||||||
|
{DDA30925-F844-426B-8B90-3E6E258BD407} = {17A95D6E-D7F8-40F0-A140-1556492495DB}
|
||||||
|
{461E9D5A-3C06-4CCB-A466-76BBB9BB7BF5} = {17A95D6E-D7F8-40F0-A140-1556492495DB}
|
||||||
|
{0098D0A8-0273-46F1-9FE7-B0409442251A} = {17A95D6E-D7F8-40F0-A140-1556492495DB}
|
||||||
|
{CA107B5D-4B8E-4515-8380-CB474C57F79C} = {17A95D6E-D7F8-40F0-A140-1556492495DB}
|
||||||
|
{00E8AC4F-B7B3-4DC5-8BA1-BB7DDC4A9071} = {0CA49C56-9635-4BA7-A88A-DA16264E2ED2}
|
||||||
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {8B677B7D-5CF6-460E-B4F6-7AF3BD655B12}
|
SolutionGuid = {8B677B7D-5CF6-460E-B4F6-7AF3BD655B12}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
24
Readme.md
24
Readme.md
@ -1,23 +1,7 @@
|
|||||||
# YABA: Yet Another Bookmark App
|
# YABA: Yet Another Bookmark App
|
||||||
|
|
||||||
## Developer Guides
|
## Developer Guide
|
||||||
|
|
||||||
### Running Migrations
|
The application is divided into two parts. Refer to the Readme on the root of the following sections for more information:
|
||||||
When running migrations, .NET seems to be ignoring dependency injection settings. In order to get around this, be sure to add the connection string to the command like. For example, when adding the migration:
|
- API: [Readme](API/Readme.md)
|
||||||
|
- Web: [Readme](Web/Readme.md)
|
||||||
```
|
|
||||||
dotnet ef migrations add InitialMigration -p YABA.Data -s YABA.API --context YABABaseContext -- {CONNECTION_STRING_HERE}
|
|
||||||
```
|
|
||||||
|
|
||||||
When removing last migration:
|
|
||||||
```
|
|
||||||
dotnet ef migrations remove InitialMigration -p YABA.Data -s YABA.API --context YABABaseContext -- {CONNECTION_STRING_HERE}
|
|
||||||
```
|
|
||||||
|
|
||||||
When applying migrations:
|
|
||||||
```
|
|
||||||
dotnet ef database update -p YABA.Data -s YABA.API -c YABABaseContext -- { CONNECTION_STRING_HERE }
|
|
||||||
```
|
|
||||||
|
|
||||||
As per the documentation [on MSDN](https://learn.microsoft.com/en-ca/ef/core/cli/dbcontext-creation?tabs=dotnet-core-cli#from-application-services):
|
|
||||||
> The -- token directs dotnet ef to treat everything that follows as an argument and not try to parse them as options. Any extra arguments not used by dotnet ef are forwarded to the app.
|
|
||||||
1
Web/.dockerignore
Normal file
1
Web/.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
22
Web/Dockerfile
Normal file
22
Web/Dockerfile
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
FROM node:17-alpine as builder
|
||||||
|
|
||||||
|
# Set the environment variables
|
||||||
|
ARG REACT_APP_API_BASE_URL
|
||||||
|
ARG REACT_APP_AUTH0_DOMAIN
|
||||||
|
ARG REACT_APP_AUTH0_CLIENT_ID
|
||||||
|
ARG REACT_APP_AUTH0_CALLBACK_URL
|
||||||
|
ARG REACT_APP_AUTH0_AUDIENCE
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json .
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:mainline-alpine
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
RUN rm -rf ./*
|
||||||
|
COPY --from=builder /app/build .
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
|
ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
||||||
@ -68,3 +68,17 @@ This section has moved here: [https://facebook.github.io/create-react-app/docs/d
|
|||||||
### `npm run build` fails to minify
|
### `npm run build` fails to minify
|
||||||
|
|
||||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||||
|
|
||||||
|
---
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
To build a dockerized version of the Web project:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build { WEB_PROJECT_ROOT_SOURCE } -f { WEB_PROJECT_DOCKERFILE_PATH } -t { imagename:tag }
|
||||||
|
```
|
||||||
|
|
||||||
|
In order to run a container using the image built above, keep the following things in mind:
|
||||||
|
- It might be necessary to map container port 80 to another
|
||||||
|
- Environment variables actively used by the web app can be overriden when starting uip a container. They are prefixed with: `REACT_APP`
|
||||||
|
- Environment variables that are explicitly listed in `Web\Dockerfile` will have to be properly set for proper operation of the application
|
||||||
8
Web/nginx.conf
Normal file
8
Web/nginx.conf
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
30717
Web/package-lock.json
generated
Normal file
30717
Web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user