Recreated solution with web app template. Improved theme. Switched to ShadcnBlazor library

This commit is contained in:
2025-12-25 19:16:53 +01:00
parent 0cc35300f1
commit a2d4edc0e5
272 changed files with 2441 additions and 14449 deletions

View File

@@ -0,0 +1,10 @@
namespace Moonlight.Api.Configuration;
public class DatabaseOptions
{
public string Host { get; set; }
public int Port { get; set; } = 5432;
public string Username { get; set; }
public string Password { get; set; }
public string Database { get; set; }
}

View File

@@ -0,0 +1,11 @@
namespace Moonlight.Api.Configuration;
public class OidcOptions
{
public string Authority { get; set; }
public bool RequireHttpsMetadata { get; set; } = true;
public string ResponseType { get; set; } = "code";
public string[]? Scopes { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
}

View File

@@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Api.Database.Entities;
namespace Moonlight.Api.Database;
public class DataContext : DbContext
{
public DbSet<User> Users { get; set; }
private readonly IOptions<DatabaseOptions> Options;
public DataContext(IOptions<DatabaseOptions> options)
{
Options = options;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured)
return;
optionsBuilder.UseNpgsql(
$"Host={Options.Value.Host};" +
$"Port={Options.Value.Port};" +
$"Username={Options.Value.Username};" +
$"Password={Options.Value.Password};" +
$"Database={Options.Value.Database}"
);
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
namespace Moonlight.Api.Database;
public class DatabaseRepository<T> where T : class
{
private readonly DataContext DataContext;
private readonly DbSet<T> Set;
public DatabaseRepository(DataContext dataContext)
{
DataContext = dataContext;
Set = DataContext.Set<T>();
}
public IQueryable<T> Query() => Set;
public async Task<T> AddAsync(T entity)
{
var final = Set.Add(entity);
await DataContext.SaveChangesAsync();
return final.Entity;
}
public async Task UpdateAsync(T entity)
{
Set.Update(entity);
await DataContext.SaveChangesAsync();
}
public async Task RemoveAsync(T entity)
{
Set.Remove(entity);
await DataContext.SaveChangesAsync();
}
}

View File

@@ -0,0 +1,11 @@
namespace Moonlight.Api.Database.Entities;
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public DateTimeOffset InvalidateTimestamp { get; set; }
}

View File

@@ -0,0 +1,55 @@
// <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 Moonlight.Api.Database;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20251216083232_AddedUsers")]
partial class AddedUsers
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("InvalidateTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
/// <inheritdoc />
public partial class AddedUsers : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy",
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Username = table.Column<string>(type: "text", nullable: false),
Email = table.Column<string>(type: "text", nullable: false),
InvalidateTimestamp =
table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table => { table.PrimaryKey("PK_Users", x => x.Id); });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
}
}
}

View File

@@ -0,0 +1,52 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Moonlight.Api.Database;
#nullable disable
namespace Moonlight.Api.Database.Migrations
{
[DbContext(typeof(DataContext))]
partial class DataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<DateTimeOffset>("InvalidateTimestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,77 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
namespace Moonlight.Api.Helpers;
public class AppConsoleFormatter : ConsoleFormatter
{
private const string TimestampColor = "\e[38;2;148;148;148m";
private const string CategoryColor = "\e[38;2;198;198;198m";
private const string MessageColor = "\e[38;2;255;255;255m";
private const string Bold = "\e[1m";
// Pre-computed ANSI color codes for each log level
private const string CriticalColor = "\e[38;2;255;0;0m";
private const string ErrorColor = "\e[38;2;255;0;0m";
private const string WarningColor = "\e[38;2;215;215;0m";
private const string InfoColor = "\e[38;2;135;215;255m";
private const string DebugColor = "\e[38;2;198;198;198m";
private const string TraceColor = "\e[38;2;68;68;68m";
public AppConsoleFormatter() : base(nameof(AppConsoleFormatter))
{
}
public override void Write<TState>(
in LogEntry<TState> logEntry,
IExternalScopeProvider? scopeProvider,
TextWriter textWriter)
{
var message = logEntry.Formatter(logEntry.State, logEntry.Exception);
// Timestamp
textWriter.Write(TimestampColor);
textWriter.Write(DateTime.Now.ToString("dd.MM.yy HH:mm:ss"));
textWriter.Write(' ');
// Log level with color and bold
var (levelText, levelColor) = GetLevelInfo(logEntry.LogLevel);
textWriter.Write(levelColor);
textWriter.Write(Bold);
textWriter.Write(levelText);
textWriter.Write(' ');
// Category
textWriter.Write(CategoryColor);
textWriter.Write(logEntry.Category);
// Message
textWriter.Write(MessageColor);
textWriter.Write(": ");
textWriter.Write(message);
// Exception
if (logEntry.Exception != null)
{
textWriter.Write(MessageColor);
textWriter.WriteLine(logEntry.Exception.ToString());
}
else
textWriter.WriteLine();
}
private static (string text, string color) GetLevelInfo(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Critical => ("CRIT", CriticalColor),
LogLevel.Error => ("ERRO", ErrorColor),
LogLevel.Warning => ("WARN", WarningColor),
LogLevel.Information => ("INFO", InfoColor),
LogLevel.Debug => ("DEBG", DebugColor),
LogLevel.Trace => ("TRCE", TraceColor),
_ => ("NONE", "")
};
}
}

View File

@@ -0,0 +1,65 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Shared.Http.Responses.Auth;
namespace Moonlight.Api.Http.Controllers;
[ApiController]
[Route("api/auth")]
public class AuthController : Controller
{
private readonly IAuthenticationSchemeProvider SchemeProvider;
public AuthController(IAuthenticationSchemeProvider schemeProvider)
{
SchemeProvider = schemeProvider;
}
[HttpGet]
public async Task<ActionResult<SchemeResponse[]>> GetSchemesAsync()
{
var schemes = await SchemeProvider.GetAllSchemesAsync();
return schemes
.Where(scheme => !string.IsNullOrWhiteSpace(scheme.DisplayName))
.Select(scheme => new SchemeResponse(scheme.Name, scheme.DisplayName!))
.ToArray();
}
[HttpGet("{schemeName:alpha}")]
public async Task<ActionResult> StartAsync([FromRoute] string schemeName)
{
var scheme = await SchemeProvider.GetSchemeAsync(schemeName);
if (scheme == null || string.IsNullOrWhiteSpace(scheme.DisplayName))
return Problem("Invalid authentication scheme name", statusCode: 400);
return Challenge(new AuthenticationProperties()
{
RedirectUri = "/"
}, scheme.Name);
}
[Authorize]
[HttpGet("claims")]
public Task<ActionResult<ClaimResponse[]>> GetClaimsAsync()
{
var result = User.Claims
.Select(claim => new ClaimResponse(claim.Type, claim.Value))
.ToArray();
return Task.FromResult<ActionResult<ClaimResponse[]>>(result);
}
[HttpGet("logout")]
public Task<ActionResult> LogoutAsync()
{
return Task.FromResult<ActionResult>(
SignOut(new AuthenticationProperties()
{
RedirectUri = "/"
})
);
}
}

View File

@@ -0,0 +1,126 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Users;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Users;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers;
namespace Moonlight.Api.Http.Controllers;
[Authorize]
[ApiController]
[Route("api/users")]
public class UsersController : Controller
{
private readonly DatabaseRepository<User> UserRepository;
public UsersController(DatabaseRepository<User> userRepository)
{
UserRepository = userRepository;
}
[HttpGet]
public async Task<ActionResult<PagedData<UserResponse>>> GetAsync(
[FromQuery] int startIndex,
[FromQuery] int length,
[FromQuery] FilterOptions? filterOptions
)
{
// Validation
if (startIndex < 0)
return Problem("Invalid start index specified", statusCode: 400);
if (length is < 1 or > 100)
return Problem("Invalid length specified");
var query = UserRepository
.Query();
// Filters
if (filterOptions != null)
{
foreach (var filterOption in filterOptions.Filters)
{
query = filterOption.Key switch
{
nameof(Database.Entities.User.Email) =>
query.Where(user => EF.Functions.ILike(user.Email, $"%{filterOption.Value}%")),
nameof(Database.Entities.User.Username) =>
query.Where(user => EF.Functions.ILike(user.Username, $"%{filterOption.Value}%")),
_ => query
};
}
}
// Pagination
var data = await query
.ProjectToResponse()
.Skip(startIndex)
.Take(length)
.ToArrayAsync();
var total = await query.CountAsync();
return new PagedData<UserResponse>(data, total);
}
[HttpGet("{id:int}")]
public async Task<ActionResult<UserResponse>> GetAsync([FromRoute] int id)
{
var user = await UserRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (user == null)
return Problem("No user with this id found", statusCode: 404);
return UserMapper.MapToResponse(user);
}
[HttpPost]
public async Task<ActionResult<UserResponse>> CreateAsync([FromBody] CreateUserRequest request)
{
var user = UserMapper.MapToUser(request);
user.InvalidateTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1);
var finalUser = await UserRepository.AddAsync(user);
return UserMapper.MapToResponse(finalUser);
}
[HttpPatch("{id:int}")]
public async Task<ActionResult<UserResponse>> UpdateAsync([FromRoute] int id, [FromBody] UpdateUserRequest request)
{
var user = await UserRepository
.Query()
.FirstOrDefaultAsync(x => x.Id == id);
if (user == null)
return Problem("No user with this id found", statusCode: 404);
UserMapper.Merge(user, request);
await UserRepository.UpdateAsync(user);
return UserMapper.MapToResponse(user);
}
[HttpDelete("{id:int}")]
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
{
var user = await UserRepository
.Query()
.FirstOrDefaultAsync(user => user.Id == id);
if (user == null)
return Problem("No user with this id found", statusCode: 404);
await UserRepository.RemoveAsync(user);
return NoContent();
}
}

View File

@@ -0,0 +1,21 @@
using System.Diagnostics.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Moonlight.Shared.Http.Requests.Users;
using Moonlight.Shared.Http.Responses.Users;
using Moonlight.Api.Database.Entities;
namespace Moonlight.Api.Mappers;
[Mapper]
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
public static partial class UserMapper
{
public static partial IQueryable<UserResponse> ProjectToResponse(this IQueryable<User> users);
public static partial UserResponse MapToResponse(User user);
public static partial void Merge([MappingTarget] User user, UpdateUserRequest request);
public static partial User MapToUser(CreateUserRequest request);
}

View File

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/>
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
</ItemGroup>
<ItemGroup>
<Folder Include="Database\Migrations\"/>
</ItemGroup>
<ItemGroup>
<Content Update="@(Content)">
<Visible Condition="'%(NuGetItemType)' == 'Content'">false</Visible>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5185",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Moonlight.Api.Database;
namespace Moonlight.Api.Services;
public class DbMigrationService : IHostedLifecycleService
{
private readonly ILogger<DbMigrationService> Logger;
private readonly IServiceProvider ServiceProvider;
public DbMigrationService(ILogger<DbMigrationService> logger, IServiceProvider serviceProvider)
{
Logger = logger;
ServiceProvider = serviceProvider;
}
public async Task StartingAsync(CancellationToken cancellationToken)
{
Logger.LogTrace("Checking for pending migrations");
await using var scope = ServiceProvider.CreateAsyncScope();
var context = scope.ServiceProvider.GetRequiredService<DataContext>();
var pendingMigrations = await context.Database.GetPendingMigrationsAsync(cancellationToken);
var migrationNames = pendingMigrations.ToArray();
if (migrationNames.Length == 0)
{
Logger.LogDebug("No pending migrations found");
return;
}
Logger.LogInformation("Pending migrations: {names}", string.Join(", ", migrationNames));
Logger.LogInformation("Migration started");
await context.Database.MigrateAsync(cancellationToken);
Logger.LogInformation("Migration complete");
}
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View File

@@ -0,0 +1,99 @@
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
namespace Moonlight.Api.Services;
public class UserAuthService
{
private readonly DatabaseRepository<User> UserRepository;
private readonly ILogger<UserAuthService> Logger;
private const string UserIdClaim = "UserId";
private const string IssuedAtClaim = "IssuedAt";
public UserAuthService(DatabaseRepository<User> userRepository, ILogger<UserAuthService> logger)
{
UserRepository = userRepository;
Logger = logger;
}
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
{
if (principal is null)
return false;
var username = principal.FindFirstValue(ClaimTypes.Name);
var email = principal.FindFirstValue(ClaimTypes.Email);
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(email))
{
Logger.LogWarning("Unable to sync user to database as name and/or email claims are missing");
return false;
}
// We use email as the primary identifier here
var user = await UserRepository
.Query()
.AsNoTracking()
.FirstOrDefaultAsync(user => user.Email == email);
if (user == null) // Sync user if not already existing in the database
{
user = await UserRepository.AddAsync(new User()
{
Username = username,
Email = email,
InvalidateTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1)
});
}
else // Update properties of existing user
{
user.Username = username;
await UserRepository.UpdateAsync(user);
}
principal.Identities.First().AddClaims([
new Claim(UserIdClaim, user.Id.ToString()),
new Claim(IssuedAtClaim, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
]);
return true;
}
public async Task<bool> ValidateAsync(ClaimsPrincipal? principal)
{
// Ignore malformed claims principal
if (principal is not { Identity.IsAuthenticated: true })
return false;
var userIdString = principal.FindFirstValue(UserIdClaim);
if (!int.TryParse(userIdString, out var userId))
return false;
var user = await UserRepository
.Query()
.AsNoTracking()
.FirstOrDefaultAsync(user => user.Id == userId);
if (user == null)
return false;
var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
if (!long.TryParse(issuedAtString, out var issuedAtUnix))
return false;
var issuedAt = DateTimeOffset.FromUnixTimeSeconds(issuedAtUnix).ToUniversalTime();
// If the issued at timestamp is greater than the token validation timestamp
// everything is fine. If not it means that the token should be invalidated
// as it is too old
return issuedAt > user.InvalidateTimestamp;
}
}

View File

@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Builder;
namespace Moonlight.Api.Startup;
public interface IAppStartup
{
public void PreBuild(WebApplicationBuilder builder);
public void PostBuild(WebApplication application);
public void PostMiddleware(WebApplication application);
}

View File

@@ -0,0 +1,91 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Moonlight.Api.Configuration;
using Moonlight.Api.Services;
namespace Moonlight.Api.Startup;
public partial class Startup
{
private static void AddAuth(WebApplicationBuilder builder)
{
var oidcOptions = new OidcOptions();
builder.Configuration.GetSection("WebApp:Oidc").Bind(oidcOptions);
builder.Services.AddScoped<UserAuthService>();
builder.Services.AddAuthentication("Session")
.AddCookie("Session", null, options =>
{
options.Events.OnSigningIn += async context =>
{
var authService = context
.HttpContext
.RequestServices
.GetRequiredService<UserAuthService>();
var result = await authService.SyncAsync(context.Principal);
if (result)
context.Properties.IsPersistent = true;
else
context.Principal = new ClaimsPrincipal();
};
options.Events.OnValidatePrincipal += async context =>
{
var authService = context
.HttpContext
.RequestServices
.GetRequiredService<UserAuthService>();
var result = await authService.ValidateAsync(context.Principal);
if (!result)
context.RejectPrincipal();
};
options.Cookie = new CookieBuilder()
{
Name = "token",
Path = "/",
IsEssential = true,
SecurePolicy = CookieSecurePolicy.SameAsRequest
};
})
.AddOpenIdConnect("OIDC", "OpenID Connect", options =>
{
options.Authority = oidcOptions.Authority;
options.RequireHttpsMetadata = oidcOptions.RequireHttpsMetadata;
var scopes = oidcOptions.Scopes ?? ["openid", "email", "profile"];
options.Scope.Clear();
foreach (var scope in scopes)
options.Scope.Add(scope);
options.ResponseType = oidcOptions.ResponseType;
options.ClientId = oidcOptions.ClientId;
options.ClientSecret = oidcOptions.ClientSecret;
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "preferred_username");
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
options.GetClaimsFromUserInfoEndpoint = true;
});
builder.Services.AddAuthorization();
}
private static void UseAuth(WebApplication application)
{
application.UseAuthentication();
application.UseAuthorization();
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Moonlight.Shared.Http;
using Moonlight.Api.Helpers;
namespace Moonlight.Api.Startup;
public partial class Startup
{
private static void AddBase(WebApplicationBuilder builder)
{
builder.Services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
});
builder.Logging.ClearProviders();
builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>();
}
private static void UseBase(WebApplication application)
{
application.UseRouting();
}
private static void MapBase(WebApplication application)
{
application.MapControllers();
application.MapFallbackToFile("index.html");
}
}

View File

@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Moonlight.Api.Configuration;
using Moonlight.Api.Database;
using Moonlight.Api.Services;
namespace Moonlight.Api.Startup;
public partial class Startup
{
private static void AddDatabase(WebApplicationBuilder builder)
{
builder.Services.AddOptions<DatabaseOptions>().BindConfiguration("WebApp:Database");
builder.Services.AddDbContext<DataContext>();
builder.Services.AddScoped(typeof(DatabaseRepository<>));
builder.Services.AddHostedService<DbMigrationService>();
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Builder;
namespace Moonlight.Api.Startup;
public partial class Startup : IAppStartup
{
public void PreBuild(WebApplicationBuilder builder)
{
AddBase(builder);
AddAuth(builder);
AddDatabase(builder);
}
public void PostBuild(WebApplication application)
{
UseBase(application);
UseAuth(application);
}
public void PostMiddleware(WebApplication application)
{
MapBase(application);
}
}