diff --git a/FictionArchive.Service.UserNovelDataService/Dockerfile b/FictionArchive.Service.UserNovelDataService/Dockerfile
new file mode 100644
index 0000000..8d7ad0d
--- /dev/null
+++ b/FictionArchive.Service.UserNovelDataService/Dockerfile
@@ -0,0 +1,23 @@
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
+USER $APP_UID
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["FictionArchive.Service.UserNovelDataService/FictionArchive.Service.UserNovelDataService.csproj", "FictionArchive.Service.UserNovelDataService/"]
+RUN dotnet restore "FictionArchive.Service.UserNovelDataService/FictionArchive.Service.UserNovelDataService.csproj"
+COPY . .
+WORKDIR "/src/FictionArchive.Service.UserNovelDataService"
+RUN dotnet build "./FictionArchive.Service.UserNovelDataService.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./FictionArchive.Service.UserNovelDataService.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "FictionArchive.Service.UserNovelDataService.dll"]
diff --git a/FictionArchive.Service.UserNovelDataService/FictionArchive.Service.UserNovelDataService.csproj b/FictionArchive.Service.UserNovelDataService/FictionArchive.Service.UserNovelDataService.csproj
new file mode 100644
index 0000000..6bee3ff
--- /dev/null
+++ b/FictionArchive.Service.UserNovelDataService/FictionArchive.Service.UserNovelDataService.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+ Linux
+
+
+
+
+ .dockerignore
+
+
+
+
+
+
+
+
diff --git a/FictionArchive.Service.UserNovelDataService/GraphQL/Mutation.cs b/FictionArchive.Service.UserNovelDataService/GraphQL/Mutation.cs
new file mode 100644
index 0000000..5810b81
--- /dev/null
+++ b/FictionArchive.Service.UserNovelDataService/GraphQL/Mutation.cs
@@ -0,0 +1,6 @@
+namespace FictionArchive.Service.UserNovelDataService.GraphQL;
+
+public class Mutation
+{
+
+}
\ No newline at end of file
diff --git a/FictionArchive.Service.UserNovelDataService/GraphQL/Query.cs b/FictionArchive.Service.UserNovelDataService/GraphQL/Query.cs
new file mode 100644
index 0000000..7ad2845
--- /dev/null
+++ b/FictionArchive.Service.UserNovelDataService/GraphQL/Query.cs
@@ -0,0 +1,6 @@
+namespace FictionArchive.Service.UserNovelDataService.GraphQL;
+
+public class Query
+{
+
+}
\ No newline at end of file
diff --git a/FictionArchive.Service.UserNovelDataService/Program.cs b/FictionArchive.Service.UserNovelDataService/Program.cs
new file mode 100644
index 0000000..166735f
--- /dev/null
+++ b/FictionArchive.Service.UserNovelDataService/Program.cs
@@ -0,0 +1,75 @@
+using FictionArchive.Common.Extensions;
+using FictionArchive.Service.Shared;
+using FictionArchive.Service.Shared.Extensions;
+using FictionArchive.Service.Shared.Services.EventBus.Implementations;
+using FictionArchive.Service.UserNovelDataService.GraphQL;
+using FictionArchive.Service.UserNovelDataService.Services;
+
+namespace FictionArchive.Service.UserNovelDataService;
+
+public class Program
+{
+ public static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ var isSchemaExport = SchemaExportDetector.IsSchemaExportMode(args);
+
+ builder.AddLocalAppsettings();
+
+ builder.Services.AddMemoryCache();
+ builder.Services.AddHealthChecks();
+
+ #region Event Bus
+
+ if (!isSchemaExport)
+ {
+ builder.Services.AddRabbitMQ(opt =>
+ {
+ builder.Configuration.GetSection("RabbitMQ").Bind(opt);
+ });
+ }
+
+ #endregion
+
+ #region GraphQL
+
+ builder.Services.AddDefaultGraphQl()
+ .AddAuthorization();
+
+ #endregion
+
+ #region Database
+
+ builder.Services.RegisterDbContext(
+ builder.Configuration.GetConnectionString("DefaultConnection"),
+ skipInfrastructure: isSchemaExport);
+
+ #endregion
+
+ // Authentication & Authorization
+ builder.Services.AddOidcAuthentication(builder.Configuration);
+ builder.Services.AddFictionArchiveAuthorization();
+
+ var app = builder.Build();
+
+ // Update database (skip in schema export mode)
+ if (!isSchemaExport)
+ {
+ using var scope = app.Services.CreateScope();
+ var dbContext = scope.ServiceProvider.GetRequiredService();
+ dbContext.UpdateDatabase();
+ }
+
+ app.UseHttpsRedirection();
+
+ app.MapHealthChecks("/healthz");
+
+ app.UseAuthentication();
+ app.UseAuthorization();
+
+ app.MapGraphQL();
+
+ app.RunWithGraphQLCommands(args);
+ }
+}
\ No newline at end of file
diff --git a/FictionArchive.Service.UserNovelDataService/Properties/launchSettings.json b/FictionArchive.Service.UserNovelDataService/Properties/launchSettings.json
new file mode 100644
index 0000000..7d694ee
--- /dev/null
+++ b/FictionArchive.Service.UserNovelDataService/Properties/launchSettings.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:26318",
+ "sslPort": 44303
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5130",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7298;http://localhost:5130",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/FictionArchive.Service.UserNovelDataService/Services/UserNovelDataServiceDbContext.cs b/FictionArchive.Service.UserNovelDataService/Services/UserNovelDataServiceDbContext.cs
new file mode 100644
index 0000000..78383ca
--- /dev/null
+++ b/FictionArchive.Service.UserNovelDataService/Services/UserNovelDataServiceDbContext.cs
@@ -0,0 +1,11 @@
+using FictionArchive.Service.Shared.Services.Database;
+using Microsoft.EntityFrameworkCore;
+
+namespace FictionArchive.Service.UserNovelDataService.Services;
+
+public class UserNovelDataServiceDbContext : FictionArchiveDbContext
+{
+ public UserNovelDataServiceDbContext(DbContextOptions options, ILogger logger) : base(options, logger)
+ {
+ }
+}
\ No newline at end of file
diff --git a/FictionArchive.Service.UserNovelDataService/appsettings.Development.json b/FictionArchive.Service.UserNovelDataService/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/FictionArchive.Service.UserNovelDataService/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/FictionArchive.Service.UserNovelDataService/appsettings.json b/FictionArchive.Service.UserNovelDataService/appsettings.json
new file mode 100644
index 0000000..baea518
--- /dev/null
+++ b/FictionArchive.Service.UserNovelDataService/appsettings.json
@@ -0,0 +1,16 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "ConnectionStrings": {
+ "DefaultConnection": "Host=localhost;Database=FictionArchive_UserNovelDataService;Username=postgres;password=postgres"
+ },
+ "RabbitMQ": {
+ "ConnectionString": "amqp://localhost",
+ "ClientIdentifier": "UserNovelDataService"
+ },
+ "AllowedHosts": "*"
+}
diff --git a/FictionArchive.sln b/FictionArchive.sln
index 6a0e6ef..8d09302 100644
--- a/FictionArchive.sln
+++ b/FictionArchive.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-#
+#
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Common", "FictionArchive.Common\FictionArchive.Common.csproj", "{ABF1BA10-9E76-45BE-9947-E20445A68147}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.API", "FictionArchive.API\FictionArchive.API.csproj", "{420CC1A1-9DBC-40EC-B9E3-D4B25D71B9A9}"
@@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Service.Nove
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Service.UserService.Tests", "FictionArchive.Service.UserService.Tests\FictionArchive.Service.UserService.Tests.csproj", "{10C38C89-983D-4544-8911-F03099F66AB8}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Service.UserNovelDataService", "FictionArchive.Service.UserNovelDataService\FictionArchive.Service.UserNovelDataService.csproj", "{A278565B-D440-4AB9-B2E2-41BA3B3AD82A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -67,5 +69,9 @@ Global
{10C38C89-983D-4544-8911-F03099F66AB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10C38C89-983D-4544-8911-F03099F66AB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10C38C89-983D-4544-8911-F03099F66AB8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A278565B-D440-4AB9-B2E2-41BA3B3AD82A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A278565B-D440-4AB9-B2E2-41BA3B3AD82A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A278565B-D440-4AB9-B2E2-41BA3B3AD82A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A278565B-D440-4AB9-B2E2-41BA3B3AD82A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal