init with code from Foxnouns.NET
This commit is contained in:
commit
e658366473
30 changed files with 1387 additions and 0 deletions
27
Hydra.Backend/Config.cs
Normal file
27
Hydra.Backend/Config.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using Serilog.Events;
|
||||
|
||||
namespace Hydra.Backend;
|
||||
|
||||
public class Config
|
||||
{
|
||||
public string Host { get; init; } = "localhost";
|
||||
public int Port { get; init; } = 3000;
|
||||
public string Address => $"http://{Host}:{Port}";
|
||||
|
||||
public LoggingConfig Logging { get; init; } = new();
|
||||
public DatabaseConfig Database { get; init; } = new();
|
||||
|
||||
public class LoggingConfig
|
||||
{
|
||||
public LogEventLevel LogEventLevel { get; init; } = LogEventLevel.Debug;
|
||||
public string? SeqLogUrl { get; init; }
|
||||
public bool LogQueries { get; init; } = false;
|
||||
}
|
||||
|
||||
public class DatabaseConfig
|
||||
{
|
||||
public string Url { get; init; } = string.Empty;
|
||||
public int? Timeout { get; init; }
|
||||
public int? MaxPoolSize { get; init; }
|
||||
}
|
||||
}
|
6
Hydra.Backend/Database/BaseModel.cs
Normal file
6
Hydra.Backend/Database/BaseModel.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace Hydra.Backend.Database;
|
||||
|
||||
public abstract class BaseModel
|
||||
{
|
||||
public Ulid Id { get; init; } = Ulid.NewUlid();
|
||||
}
|
73
Hydra.Backend/Database/HydraContext.cs
Normal file
73
Hydra.Backend/Database/HydraContext.cs
Normal file
|
@ -0,0 +1,73 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using EntityFramework.Exceptions.PostgreSQL;
|
||||
using Hydra.Backend.Database.Models;
|
||||
using Hydra.Backend.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Npgsql;
|
||||
|
||||
namespace Hydra.Backend.Database;
|
||||
|
||||
public class HydraContext : DbContext
|
||||
{
|
||||
private readonly NpgsqlDataSource _dataSource;
|
||||
private readonly ILoggerFactory? _loggerFactory;
|
||||
|
||||
public DbSet<Account> Accounts { get; set; }
|
||||
public DbSet<Member> Members { get; set; }
|
||||
public DbSet<Token> Tokens { get; set; }
|
||||
public DbSet<Application> Applications { get; set; }
|
||||
|
||||
public HydraContext(Config config, ILoggerFactory? loggerFactory)
|
||||
{
|
||||
var connString = new NpgsqlConnectionStringBuilder(config.Database.Url)
|
||||
{
|
||||
Timeout = config.Database.Timeout ?? 5,
|
||||
MaxPoolSize = config.Database.MaxPoolSize ?? 50,
|
||||
}.ConnectionString;
|
||||
|
||||
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString);
|
||||
dataSourceBuilder.UseNodaTime();
|
||||
_dataSource = dataSourceBuilder.Build();
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
=> optionsBuilder
|
||||
.ConfigureWarnings(c =>
|
||||
c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)
|
||||
.Ignore(CoreEventId.SaveChangesFailed))
|
||||
.UseNpgsql(_dataSource, o => o.UseNodaTime())
|
||||
.UseSnakeCaseNamingConvention()
|
||||
.UseLoggerFactory(_loggerFactory)
|
||||
.UseExceptionProcessor();
|
||||
|
||||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
|
||||
{
|
||||
// ULIDs are stored as UUIDs in the database
|
||||
configurationBuilder.Properties<Ulid>().HaveConversion<UlidConverter>();
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Account>().HasIndex(u => u.Name).IsUnique();
|
||||
modelBuilder.Entity<Member>().HasIndex(m => new { m.AccountId, m.Name }).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "UnusedType.Global", Justification = "Used when generating migrations")]
|
||||
public class DesignTimeDatabaseContextFactory : IDesignTimeDbContextFactory<HydraContext>
|
||||
{
|
||||
public HydraContext CreateDbContext(string[] args)
|
||||
{
|
||||
// Read the configuration file
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddConfiguration(args)
|
||||
.Build()
|
||||
// Get the configuration as our config class
|
||||
.Get<Config>() ?? new();
|
||||
|
||||
return new HydraContext(config, null);
|
||||
}
|
||||
}
|
10
Hydra.Backend/Database/Models/Account.cs
Normal file
10
Hydra.Backend/Database/Models/Account.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace Hydra.Backend.Database.Models;
|
||||
|
||||
public class Account : BaseModel
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required string EmailAddress { get; set; }
|
||||
public required string Password { get; set; }
|
||||
|
||||
public List<Member> Members { get; init; } = [];
|
||||
}
|
39
Hydra.Backend/Database/Models/Application.cs
Normal file
39
Hydra.Backend/Database/Models/Application.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System.Security.Cryptography;
|
||||
using Hydra.Backend.Utils;
|
||||
|
||||
namespace Hydra.Backend.Database.Models;
|
||||
|
||||
public class Application : BaseModel
|
||||
{
|
||||
public required string ClientId { get; init; }
|
||||
public required string ClientSecret { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required string[] Scopes { get; init; }
|
||||
public required string[] RedirectUris { get; set; }
|
||||
|
||||
public static Application Create(string name, string[] scopes,
|
||||
string[] redirectUrls)
|
||||
{
|
||||
var clientId = RandomNumberGenerator.GetHexString(32, true);
|
||||
var clientSecret = AuthUtils.RandomToken();
|
||||
|
||||
if (scopes.Except(AuthUtils.Scopes).Any())
|
||||
{
|
||||
throw new ArgumentException("Invalid scopes passed to Application.Create", nameof(scopes));
|
||||
}
|
||||
|
||||
if (redirectUrls.Any(s => !AuthUtils.ValidateRedirectUri(s)))
|
||||
{
|
||||
throw new ArgumentException("Invalid redirect URLs passed to Application.Create", nameof(redirectUrls));
|
||||
}
|
||||
|
||||
return new Application
|
||||
{
|
||||
ClientId = clientId,
|
||||
ClientSecret = clientSecret,
|
||||
Name = name,
|
||||
Scopes = scopes,
|
||||
RedirectUris = redirectUrls
|
||||
};
|
||||
}
|
||||
}
|
9
Hydra.Backend/Database/Models/Member.cs
Normal file
9
Hydra.Backend/Database/Models/Member.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Hydra.Backend.Database.Models;
|
||||
|
||||
public class Member : BaseModel
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
|
||||
public Ulid AccountId { get; init; }
|
||||
public Account Account { get; init; } = null!;
|
||||
}
|
17
Hydra.Backend/Database/Models/Token.cs
Normal file
17
Hydra.Backend/Database/Models/Token.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using NodaTime;
|
||||
|
||||
namespace Hydra.Backend.Database.Models;
|
||||
|
||||
public class Token : BaseModel
|
||||
{
|
||||
public required byte[] Hash { get; init; }
|
||||
public required Instant ExpiresAt { get; init; }
|
||||
public required string[] Scopes { get; init; }
|
||||
public bool ManuallyExpired { get; set; }
|
||||
|
||||
public Ulid UserId { get; init; }
|
||||
public Account Account { get; init; } = null!;
|
||||
|
||||
public Ulid ApplicationId { get; set; }
|
||||
public Application Application { get; set; } = null!;
|
||||
}
|
11
Hydra.Backend/Database/UlidConverter.cs
Normal file
11
Hydra.Backend/Database/UlidConverter.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace Hydra.Backend.Database;
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global",
|
||||
Justification = "Referenced in HydraContext.ConfigureConventions")]
|
||||
public class UlidConverter() : ValueConverter<Ulid, Guid>(
|
||||
convertToProviderExpression: x => x.ToGuid(),
|
||||
convertFromProviderExpression: x => new Ulid(x)
|
||||
);
|
23
Hydra.Backend/Dockerfile
Normal file
23
Hydra.Backend/Dockerfile
Normal file
|
@ -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 ["Hydra.Backend/Hydra.Backend.csproj", "Hydra.Backend/"]
|
||||
RUN dotnet restore "Hydra.Backend/Hydra.Backend.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Hydra.Backend"
|
||||
RUN dotnet build "Hydra.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "Hydra.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Hydra.Backend.dll"]
|
3
Hydra.Backend/GlobalUsing.cs
Normal file
3
Hydra.Backend/GlobalUsing.cs
Normal file
|
@ -0,0 +1,3 @@
|
|||
global using ILogger = Serilog.ILogger;
|
||||
global using Log = Serilog.Log;
|
||||
global using NUlid;
|
36
Hydra.Backend/Hydra.Backend.csproj
Normal file
36
Hydra.Backend/Hydra.Backend.csproj
Normal file
|
@ -0,0 +1,36 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
|
||||
<PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="8.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="8.0.4" />
|
||||
<PackageReference Include="NUlid" Version="1.7.2" />
|
||||
<PackageReference Include="Serilog" Version="4.0.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
|
||||
<PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
29
Hydra.Backend/HydraError.cs
Normal file
29
Hydra.Backend/HydraError.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using System.Net;
|
||||
|
||||
namespace Hydra.Backend;
|
||||
|
||||
public class HydraError(string message, Exception? inner = null) : Exception(message)
|
||||
{
|
||||
public Exception? Inner => inner;
|
||||
|
||||
public class DatabaseError(string message, Exception? inner = null) : HydraError(message, inner);
|
||||
|
||||
public class UnknownEntityError(Type entityType, Exception? inner = null)
|
||||
: DatabaseError($"Entity of type {entityType.Name} not found", inner);
|
||||
}
|
||||
|
||||
public class ApiError(string message, HttpStatusCode? statusCode = null)
|
||||
: HydraError(message)
|
||||
{
|
||||
public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError;
|
||||
|
||||
public class Unauthorized(string message) : ApiError(message, statusCode: HttpStatusCode.Unauthorized);
|
||||
|
||||
public class Forbidden(string message, IEnumerable<string>? scopes = null)
|
||||
: ApiError(message, statusCode: HttpStatusCode.Forbidden)
|
||||
{
|
||||
public readonly string[] Scopes = scopes?.ToArray() ?? [];
|
||||
}
|
||||
|
||||
public class AuthenticationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
||||
}
|
66
Hydra.Backend/Middleware/AuthenticationMiddleware.cs
Normal file
66
Hydra.Backend/Middleware/AuthenticationMiddleware.cs
Normal file
|
@ -0,0 +1,66 @@
|
|||
using System.Security.Cryptography;
|
||||
using Hydra.Backend.Database;
|
||||
using Hydra.Backend.Database.Models;
|
||||
using Hydra.Backend.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Hydra.Backend.Middleware;
|
||||
|
||||
public class AuthenticationMiddleware(HydraContext db, IClock clock) : IMiddleware
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
|
||||
{
|
||||
var endpoint = ctx.GetEndpoint();
|
||||
var metadata = endpoint?.Metadata.GetMetadata<AuthenticateAttribute>();
|
||||
|
||||
if (metadata == null)
|
||||
{
|
||||
await next(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
var header = ctx.Request.Headers.Authorization.ToString();
|
||||
if (!AuthUtils.TryFromBase64String(header, out var rawToken))
|
||||
{
|
||||
await next(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
var hash = SHA512.HashData(rawToken);
|
||||
var oauthToken = await db.Tokens
|
||||
.Include(t => t.Application)
|
||||
.Include(t => t.Account)
|
||||
.FirstOrDefaultAsync(t => t.Hash == hash && t.ExpiresAt > clock.GetCurrentInstant() && !t.ManuallyExpired);
|
||||
if (oauthToken == null)
|
||||
{
|
||||
await next(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.SetToken(oauthToken);
|
||||
|
||||
await next(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
public static class HttpContextExtensions
|
||||
{
|
||||
private const string Key = "token";
|
||||
|
||||
public static void SetToken(this HttpContext ctx, Token token) => ctx.Items.Add(Key, token);
|
||||
public static Account? GetAccount(this HttpContext ctx) => ctx.GetToken()?.Account;
|
||||
|
||||
public static Account GetAccountOrThrow(this HttpContext ctx) =>
|
||||
ctx.GetAccount() ?? throw new ApiError.AuthenticationError("No account in HttpContext");
|
||||
|
||||
public static Token? GetToken(this HttpContext ctx)
|
||||
{
|
||||
if (ctx.Items.TryGetValue(Key, out var token))
|
||||
return token as Token;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class AuthenticateAttribute : Attribute;
|
34
Hydra.Backend/Middleware/AuthorizationMiddleware.cs
Normal file
34
Hydra.Backend/Middleware/AuthorizationMiddleware.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
namespace Hydra.Backend.Middleware;
|
||||
|
||||
public class AuthorizationMiddleware : IMiddleware
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
|
||||
{
|
||||
var endpoint = ctx.GetEndpoint();
|
||||
var attribute = endpoint?.Metadata.GetMetadata<AuthorizeAttribute>();
|
||||
|
||||
if (attribute == null)
|
||||
{
|
||||
await next(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
var token = ctx.GetToken();
|
||||
if (token == null)
|
||||
throw new ApiError.Unauthorized("This endpoint requires an authenticated user.");
|
||||
if (attribute.Scopes.Length > 0 && attribute.Scopes.Except(token.Scopes).Any())
|
||||
throw new ApiError.Forbidden("This endpoint requires ungranted scopes.",
|
||||
attribute.Scopes.Except(token.Scopes));
|
||||
|
||||
await next(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class AuthorizeAttribute(params string[] scopes) : Attribute
|
||||
{
|
||||
public readonly bool RequireAdmin = scopes.Contains(":admin");
|
||||
public readonly bool RequireModerator = scopes.Contains(":moderator");
|
||||
|
||||
public readonly string[] Scopes = scopes.Except([":admin", ":moderator"]).ToArray();
|
||||
}
|
46
Hydra.Backend/Program.cs
Normal file
46
Hydra.Backend/Program.cs
Normal file
|
@ -0,0 +1,46 @@
|
|||
using Hydra.Backend.Database;
|
||||
using Hydra.Backend.Utils;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Serilog;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var config = builder.AddConfiguration(args);
|
||||
builder.AddSerilog();
|
||||
builder.WebHost.ConfigureKestrel(opts =>
|
||||
{
|
||||
// Requests are limited to a maximum of 2 MB.
|
||||
// No valid request body will ever come close to this limit,
|
||||
// but the limit is slightly higher to prevent valid requests from being rejected.
|
||||
opts.Limits.MaxRequestBodySize = 2 * 1024 * 1024;
|
||||
});
|
||||
|
||||
builder.Services
|
||||
.AddDbContext<HydraContext>()
|
||||
.AddCustomMiddleware()
|
||||
.AddEndpointsApiExplorer()
|
||||
.AddSwaggerGen()
|
||||
.AddControllers()
|
||||
.AddNewtonsoftJson(options =>
|
||||
{
|
||||
options.SerializerSettings.ContractResolver = new PatchRequestContractResolver
|
||||
{
|
||||
NamingStrategy = new SnakeCaseNamingStrategy()
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseSerilogRequestLogging();
|
||||
app.UseRouting();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
app.UseCors();
|
||||
app.UseCustomMiddleware();
|
||||
app.MapControllers();
|
||||
|
||||
app.Urls.Clear();
|
||||
app.Urls.Add(config.Address);
|
||||
|
||||
app.Run();
|
||||
Log.CloseAndFlush();
|
41
Hydra.Backend/Properties/launchSettings.json
Normal file
41
Hydra.Backend/Properties/launchSettings.json
Normal file
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:32246",
|
||||
"sslPort": 44353
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5209",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:7285;http://localhost:5209",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
51
Hydra.Backend/Utils/AuthUtils.cs
Normal file
51
Hydra.Backend/Utils/AuthUtils.cs
Normal file
|
@ -0,0 +1,51 @@
|
|||
using System.Security.Cryptography;
|
||||
using Hydra.Backend.Database.Models;
|
||||
|
||||
namespace Hydra.Backend.Utils;
|
||||
|
||||
public static class AuthUtils
|
||||
{
|
||||
public const string ClientCredentials = "client_credentials";
|
||||
public const string AuthorizationCode = "authorization_code";
|
||||
private static readonly string[] ForbiddenSchemes = ["javascript", "file", "data", "mailto", "tel"];
|
||||
|
||||
// TODO: add actual scopes
|
||||
public static readonly string[] Scopes = ["*"];
|
||||
|
||||
public static bool ValidateScopes(Application application, string[] scopes)
|
||||
{
|
||||
return !scopes.Except(application.Scopes).Any();
|
||||
}
|
||||
|
||||
public static bool ValidateRedirectUri(string uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
var scheme = new Uri(uri).Scheme;
|
||||
return !ForbiddenSchemes.Contains(scheme);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryFromBase64String(string b64, out byte[] bytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
bytes = Convert.FromBase64String(b64);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
bytes = [];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static string RandomToken(int bytes = 48) =>
|
||||
Convert.ToBase64String(RandomNumberGenerator.GetBytes(bytes)).Trim('=');
|
||||
|
||||
public const int MaxAuthMethodsPerType = 3; // Maximum of 3 Discord accounts, 3 emails, etc
|
||||
}
|
35
Hydra.Backend/Utils/PatchRequest.cs
Normal file
35
Hydra.Backend/Utils/PatchRequest.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Hydra.Backend.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// A base class used for PATCH requests which stores information on whether a key is explicitly set to null or not passed at all.
|
||||
/// </summary>
|
||||
public abstract class PatchRequest
|
||||
{
|
||||
private readonly HashSet<string> _properties = [];
|
||||
public bool HasProperty(string propertyName) => _properties.Contains(propertyName);
|
||||
public void SetHasProperty(string propertyName) => _properties.Add(propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom contract resolver to reduce the boilerplate needed to use <see cref="PatchRequest" />.
|
||||
/// Based on this StackOverflow answer: https://stackoverflow.com/a/58748036
|
||||
/// </summary>
|
||||
public class PatchRequestContractResolver : DefaultContractResolver
|
||||
{
|
||||
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
|
||||
{
|
||||
var prop = base.CreateProperty(member, memberSerialization);
|
||||
|
||||
prop.SetIsSpecified += (o, _) =>
|
||||
{
|
||||
if (o is not PatchRequest patchRequest) return;
|
||||
patchRequest.SetHasProperty(prop.UnderlyingName!);
|
||||
};
|
||||
|
||||
return prop;
|
||||
}
|
||||
}
|
73
Hydra.Backend/Utils/StartupExtensions.cs
Normal file
73
Hydra.Backend/Utils/StartupExtensions.cs
Normal file
|
@ -0,0 +1,73 @@
|
|||
using Hydra.Backend.Middleware;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Hydra.Backend.Utils;
|
||||
|
||||
public static class StartupExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Serilog to this service collection. This method also initializes Serilog, so it should be called as early as possible, before any log calls.
|
||||
/// </summary>
|
||||
public static WebApplicationBuilder AddSerilog(this WebApplicationBuilder builder)
|
||||
{
|
||||
var config = builder.Configuration.Get<Config>() ?? new();
|
||||
|
||||
var logCfg = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithEnvironmentName()
|
||||
.Enrich.WithMachineName()
|
||||
.Enrich.WithProcessId()
|
||||
.Enrich.WithProcessName()
|
||||
.MinimumLevel.Is(config.Logging.LogEventLevel)
|
||||
// ASP.NET's built in request logs are extremely verbose, so we use Serilog's instead.
|
||||
// Serilog doesn't disable the built-in logs, so we do it here.
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||
.MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command",
|
||||
config.Logging.LogQueries ? LogEventLevel.Information : LogEventLevel.Fatal)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Hangfire", LogEventLevel.Information)
|
||||
.WriteTo.Console();
|
||||
|
||||
if (config.Logging.SeqLogUrl != null)
|
||||
{
|
||||
logCfg.WriteTo.Seq(config.Logging.SeqLogUrl, restrictedToMinimumLevel: LogEventLevel.Verbose);
|
||||
}
|
||||
|
||||
// AddSerilog doesn't seem to add an ILogger to the service collection, so add that manually.
|
||||
builder.Services.AddSerilog().AddSingleton(Log.Logger = logCfg.CreateLogger());
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services
|
||||
.AddScoped<AuthenticationMiddleware>()
|
||||
.AddScoped<AuthorizationMiddleware>();
|
||||
|
||||
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app) => app
|
||||
.UseMiddleware<AuthenticationMiddleware>()
|
||||
.UseMiddleware<AuthorizationMiddleware>();
|
||||
|
||||
public static Config AddConfiguration(this WebApplicationBuilder builder, string[] args)
|
||||
{
|
||||
builder.Configuration.Sources.Clear();
|
||||
builder.Configuration.AddConfiguration(args);
|
||||
|
||||
var config = builder.Configuration.Get<Config>() ?? new();
|
||||
builder.Services.AddSingleton(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
public static IConfigurationBuilder AddConfiguration(this IConfigurationBuilder builder, string[] args)
|
||||
{
|
||||
var file = Environment.GetEnvironmentVariable("HDYRA_CONFIG_FILE") ?? "config.ini";
|
||||
|
||||
return builder
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddIniFile(file, optional: false, reloadOnChange: true)
|
||||
.AddEnvironmentVariables()
|
||||
.AddCommandLine(args);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue