15 Commits

Author SHA1 Message Date
9fae3b978f Modified ClaimNameAttribute to support multiple claim names depending on environment
All checks were successful
ci/woodpecker/tag/api_build Pipeline was successful
ci/woodpecker/tag/api_uploadimage Pipeline was successful
Environment name will be pulled from environment variable: ASPNETCORE_ENVIRONMENT
2024-04-07 20:18:53 -05:00
822a8379aa Merge pull request 'Created Woodpecker CI/CD deployment' (#38) from feature/CreateCIPlans into develop
All checks were successful
ci/woodpecker/push/api_build Pipeline was successful
Reviewed-on: #38
2024-04-07 15:20:05 -05:00
a5d5ed048f Created Woodpecker CI/CD deployment
- Created Dockerfile for packing up API and Web projects as Docker image
2024-04-07 14:51:28 -05:00
baf38aa3cd Merge pull request #28 from carltibule/feature/SwitchToStructuredLogging
Added structured logging to file and Grafana Loki
2023-03-27 21:41:06 -05:00
0ad6e99a8f Added structured logging to file and Grafana Loki 2023-03-27 21:38:43 -05:00
52273a8d12 Merge pull request #27 from carltibule/feature/AddUnitTests
Added unit tests for API and Web App
2023-03-08 21:46:13 -06:00
771b63f6c4 Added unit tests for API and Web App
Also updated backend to use .NET6
2023-03-08 21:34:43 -06:00
efb22aa0af Merge pull request #26 from carltibule/feature/RedirectHomePage
Redirected Home page to Bookmarks list page when authenticated and lo…
2023-03-05 19:01:25 -06:00
067b32826d Redirected Home page to Bookmarks list page when authenticated and login page when not
Also modified to have 200 returned when only tags are changed and not the bookmark itself
2023-03-05 18:57:46 -06:00
91c7c71299 Merge pull request #25 from carltibule/feature/ReturnBookmarksWithNoTags
Fixed issue with Bookmarks with no tags not being returned
2023-03-04 23:50:34 -06:00
e240d1fdac Fixed issue with Bookmarks with no tags not being returned 2023-03-04 23:46:57 -06:00
ba2bcb931f Merge pull request #24 from carltibule/feature/AddUserAgentToWebClientBeforeDownloadingWebPageString
Fixed 403 Forbidden thrown when getting Website metadata
2023-03-04 00:39:51 -06:00
34d459cc58 Fixed 403 Forbidden thrown when getting Website metadata
- Add User-Agent to header to prevent 403 Forbidden when downloading webpage strings
- Add Bookmark URL and User Id unique combo constraint
- Add Tag Name and User Id unique combo constraint
2023-03-04 00:37:59 -06:00
d4187b2e94 Merge pull request #21 from carltibule/feature/SwallowDuplicateRegisterRequestException
Add auto-registration mechanism
2023-03-03 23:42:21 -06:00
b0cb791bc2 Add auto-registration mechanism
- Add User registration mechanism when adding auth provider id to claims, if no User Id corresponds to the auth provider Id
- Swallow unique constraint violation for unique auth provider id on user table, in case of duplicate requests
- Add Serilog logging
- Add no bookmarks and no tags message when none is found on Bookmark List page
2023-03-03 23:35:53 -06:00
151 changed files with 31824 additions and 17893 deletions

3
.gitignore vendored
View File

@ -247,3 +247,6 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
.env
Logs/*
docker-compose.yml
*exclude*

View 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

View 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

View 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
View 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

View File

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

38
API/YABA.API/Dockerfile Normal file
View 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=Development
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"]

View File

@ -2,6 +2,7 @@
using YABA.API.Extensions;
using YABA.Common.Extensions;
using YABA.Common.Lookups;
using YABA.Common.Utils;
using YABA.Service.Interfaces;
namespace YABA.API.Middlewares
@ -24,7 +25,14 @@ namespace YABA.API.Middlewares
if (!string.IsNullOrEmpty(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()));
}
}

View File

@ -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);
}
}
}

View File

@ -12,8 +12,12 @@ using YABA.API.Settings.Swashbuckle;
using YABA.Data.Configuration;
using YABA.Data.Context;
using YABA.Service.Configuration;
using Serilog;
using Microsoft.AspNetCore.HttpOverrides;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddEnvironmentVariables();
var configuration = builder.Configuration;
// Add services to the container.
@ -48,6 +52,7 @@ builder.Services.AddHttpContextAccessor();
builder.Services.AddServiceProjectDependencyInjectionConfiguration(configuration);
builder.Services.AddDataProjectDependencyInjectionConfiguration(configuration);
builder.Services.AddControllers().AddNewtonsoftJson();
builder.Services.AddHealthChecks();
// Add AutoMapper profiles
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();
// Run database migrations
@ -95,6 +110,7 @@ if (app.Environment.IsDevelopment())
app.UseHttpsRedirection();
app.UseForwardedHeaders();
app.UseAuthentication();
app.UseAuthorization();
@ -102,13 +118,10 @@ app.MapControllers();
// Add custom middlewares
app.UseMiddleware<AddCustomClaimsMiddleware>();
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseMiddleware<AddCustomLoggingPropertiesMiddleware>();
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();

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

@ -11,13 +11,22 @@
<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
<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.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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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.Newtonsoft" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.5.0" />
@ -29,4 +38,8 @@
<ProjectReference Include="..\YABA.Service\YABA.Service.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Logs\" />
</ItemGroup>
</Project>

View 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"
}
}

View 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;
}
}
}

View File

@ -28,9 +28,9 @@ namespace YABA.Common.Extensions
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);

View File

@ -1,6 +1,8 @@
using System.Security.Claims;
using System;
using System.Security.Claims;
using System.Security.Principal;
using YABA.Common.Lookups;
using YABA.Common.Utils;
namespace YABA.Common.Extensions
{
@ -12,7 +14,7 @@ namespace YABA.Common.Extensions
public static string GetCustomClaim(this IIdentity identity, ClaimsLookup claim)
{
var claimsIdentity = identity as ClaimsIdentity;
return claimsIdentity.FindFirst(claim.GetClaimName())?.Value.ToString();
return claimsIdentity.FindFirst(claim.GetClaimName(EnvironmentUtils.IsDevelopmentEnvironment()))?.Value.ToString();
}
}
}

View 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
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace YABA.Common.Utils
{
public static class EnvironmentUtils
{
public static bool IsDevelopmentEnvironment() => Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development";
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@ -12,6 +12,7 @@ namespace YABA.Data.Context
{
public class YABABaseContext : DbContext
{
public YABABaseContext() : base() { }
public YABABaseContext(DbContextOptions<YABABaseContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
@ -39,14 +40,18 @@ namespace YABA.Data.Context
.IsUnique();
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();
}
public DbSet<Bookmark> Bookmarks { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<BookmarkTag> BookmarkTags { get; set; }
public DbSet<User> Users { get; set; }
public virtual DbSet<Bookmark> Bookmarks { get; set; }
public virtual DbSet<Tag> Tags { get; set; }
public virtual DbSet<BookmarkTag> BookmarkTags { get; set; }
public virtual DbSet<User> Users { get; set; }
private static LambdaExpression ConvertFilterExpression<TInterface>(
Expression<Func<TInterface, bool>> filterExpression,

View File

@ -4,6 +4,7 @@ namespace YABA.Data.Context
{
public class YABAReadOnlyContext : YABABaseContext
{
public YABAReadOnlyContext() : base() { }
public YABAReadOnlyContext(DbContextOptions<YABABaseContext> options) : base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

View File

@ -9,6 +9,8 @@ namespace YABA.Data.Context
{
public class YABAReadWriteContext : YABABaseContext
{
public YABAReadWriteContext() : base() { }
public YABAReadWriteContext(DbContextOptions<YABABaseContext> options) : base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;

View File

@ -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
}
}
}

View File

@ -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);
}
}
}

View File

@ -67,6 +67,10 @@ namespace YABA.Data.Migrations
b.HasIndex("UserId")
.HasDatabaseName("ix_bookmarks_user_id");
b.HasIndex("Url", "UserId")
.IsUnique()
.HasDatabaseName("ix_bookmarks_url_user_id");
b.ToTable("bookmarks");
});
@ -113,13 +117,13 @@ namespace YABA.Data.Migrations
b.HasKey("Id")
.HasName("pk_tags");
b.HasIndex("Name")
.IsUnique()
.HasDatabaseName("ix_tags_name");
b.HasIndex("UserId")
.HasDatabaseName("ix_tags_user_id");
b.HasIndex("Name", "UserId")
.IsUnique()
.HasDatabaseName("ix_tags_name_user_id");
b.ToTable("tags");
});

View File

@ -1,20 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="5.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.17" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.17">
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.14">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration" 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>

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using YABA.Common.Interfaces;
@ -21,5 +22,7 @@ namespace YABA.Models
[ForeignKey(nameof(User))]
public int UserId { get; set; }
public virtual User User { get; set; }
public ICollection<BookmarkTag> BookmarkTags { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using YABA.Models.Interfaces;
@ -14,5 +15,7 @@ namespace YABA.Models
[ForeignKey(nameof(User))]
public int UserId { get; set; }
public virtual User User { get; set; }
public virtual ICollection<BookmarkTag> TagBookmarks { get; set; }
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@ -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; }
}
}

View 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>

View File

@ -2,6 +2,7 @@
using HtmlAgilityPack;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
@ -40,26 +41,28 @@ namespace YABA.Service
public IEnumerable<BookmarkDTO> GetAll(bool showHidden = false)
{
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>();
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
|| (!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>(bookmarkTag.Value.Key);
var bookmarkTagDTOs = _mapper.Map<IEnumerable<TagDTO>>(bookmarkTag.Value.Value);
bookmarkDTO.Tags.AddRange(bookmarkTagDTOs);
userBookmarkDTOs.Add(bookmarkDTO);
}
var isBookmarkHidden = bookmark.IsHidden;
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark);
var bookmarkTags = bookmark.BookmarkTags.Select(x => x.Tag);
bookmarkDTO.Tags = _mapper.Map<List<TagDTO>>(bookmarkTags);
userBookmarkDTOs.Add(bookmarkDTO);
}
return userBookmarkDTOs;
@ -110,14 +113,10 @@ namespace YABA.Service
bookmark.Url = !string.IsNullOrEmpty(request.Url) ? request.Url : bookmark.Url;
UpdateBookmarkWithMetaData(bookmark);
if (await _context.SaveChangesAsync() > 0)
{
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark);
bookmarkDTO.Tags = tags;
return bookmarkDTO;
}
return null;
await _context.SaveChangesAsync();
var bookmarkDTO = _mapper.Map<BookmarkDTO>(bookmark);
bookmarkDTO.Tags = tags;
return bookmarkDTO;
}
public async Task<IEnumerable<TagDTO>?> UpdateBookmarkTags(int id, IEnumerable<string> tags)
@ -253,6 +252,7 @@ namespace YABA.Service
private void UpdateBookmarkWithMetaData(IBookmark bookmark)
{
var webClient = new WebClient();
webClient.Headers.Add("User-Agent", "APIClient");
var sourceData = webClient.DownloadString(bookmark.Url);
var title = Regex.Match(sourceData, @"\<title\b[^>]*\>\s*(?<Title>[\s\S]*?)\</title\>", RegexOptions.IgnoreCase).Groups["Title"].Value;
var description = string.Empty;

View 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;
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@ -11,7 +11,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.Data", "YABA.Data\YABA
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YABA.Service", "YABA.Service\YABA.Service.csproj", "{0098D0A8-0273-46F1-9FE7-B0409442251A}"
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
Global
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
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
SolutionGuid = {8B677B7D-5CF6-460E-B4F6-7AF3BD655B12}
EndGlobalSection

View File

@ -1,23 +1,7 @@
# YABA: Yet Another Bookmark App
## Developer Guides
## 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.
The application is divided into two parts. Refer to the Readme on the root of the following sections for more information:
- API: [Readme](API/Readme.md)
- Web: [Readme](Web/Readme.md)

1
Web/.dockerignore Normal file
View File

@ -0,0 +1 @@
node_modules/

22
Web/Dockerfile Normal file
View 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;"]

View File

@ -68,3 +68,17 @@ This section has moved here: [https://facebook.github.io/create-react-app/docs/d
### `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
View 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

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