Mastodon client API foundations
This commit is contained in:
parent
3f333b0c5d
commit
cec4abd841
13 changed files with 434 additions and 32 deletions
|
@ -1,3 +1,9 @@
|
|||
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
using Iceshrimp.Backend.Core.Helpers;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
|
||||
|
@ -8,4 +14,55 @@ namespace Iceshrimp.Backend.Controllers.Mastodon;
|
|||
[EnableRateLimiting("sliding")]
|
||||
[Produces("application/json")]
|
||||
[Route("/api/v1")]
|
||||
public class MastodonAuthController : Controller { }
|
||||
public class MastodonAuthController(DatabaseContext db) : Controller {
|
||||
[AuthenticateOauth]
|
||||
[HttpGet("verify_credentials")]
|
||||
[Produces("application/json")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(MastodonAuth.VerifyCredentialsResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(MastodonErrorResponse))]
|
||||
public IActionResult VerifyCredentials() {
|
||||
var token = HttpContext.GetOauthToken();
|
||||
if (token == null) throw GracefulException.Unauthorized("The access token is invalid");
|
||||
|
||||
var res = new MastodonAuth.VerifyCredentialsResponse {
|
||||
App = token.App,
|
||||
VapidKey = null //FIXME
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPost("apps")]
|
||||
[Consumes("application/json", "application/x-www-form-urlencoded", "multipart/form-data")]
|
||||
[Produces("application/json")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(MastodonAuth.RegisterAppResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(MastodonErrorResponse))]
|
||||
public async Task<IActionResult> RegisterApp([FromHybrid] MastodonAuth.RegisterAppRequest request) {
|
||||
if (request.RedirectUris.Count == 0)
|
||||
throw GracefulException.BadRequest("Invalid redirect_uris parameter");
|
||||
|
||||
if (request.RedirectUris.Any(p => !MastodonOauthHelpers.ValidateRedirectUri(p)))
|
||||
throw GracefulException.BadRequest("redirect_uris parameter contains invalid protocols");
|
||||
|
||||
var app = new OauthApp {
|
||||
Id = IdHelpers.GenerateSlowflakeId(),
|
||||
ClientId = CryptographyHelpers.GenerateRandomString(32),
|
||||
ClientSecret = CryptographyHelpers.GenerateRandomString(32),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Name = request.ClientName,
|
||||
Website = request.Website,
|
||||
Scopes = request.Scopes,
|
||||
RedirectUris = request.RedirectUris,
|
||||
};
|
||||
|
||||
await db.AddAsync(app);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var res = new MastodonAuth.RegisterAppResponse {
|
||||
App = app,
|
||||
VapidKey = null //FIXME
|
||||
};
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Helpers;
|
||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||
using JR = System.Text.Json.Serialization.JsonRequiredAttribute;
|
||||
using JC = System.Text.Json.Serialization.JsonConverterAttribute;
|
||||
using B = Microsoft.AspNetCore.Mvc.BindPropertyAttribute;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
|
||||
public abstract class MastodonAuth {
|
||||
public class VerifyCredentialsResponse {
|
||||
public required OauthApp App;
|
||||
|
||||
[J("name")] public string Name => App.Name;
|
||||
[J("website")] public string? Website => App.Website;
|
||||
|
||||
[J("vapid_key")] public required string? VapidKey { get; set; }
|
||||
}
|
||||
|
||||
public class RegisterAppRequest {
|
||||
private List<string> _scopes = ["read"];
|
||||
public List<string> RedirectUris = [];
|
||||
|
||||
[B(Name = "scopes")]
|
||||
[J("scopes")]
|
||||
[JC(typeof(EnsureArrayConverter))]
|
||||
public List<string> Scopes {
|
||||
get => _scopes;
|
||||
set => _scopes = value.Count == 1
|
||||
? value[0].Split(' ').ToList()
|
||||
: value;
|
||||
}
|
||||
|
||||
[B(Name = "client_name")]
|
||||
[J("client_name")]
|
||||
[JR]
|
||||
public string ClientName { get; set; } = null!;
|
||||
|
||||
[B(Name = "website")] [J("website")] public string? Website { get; set; }
|
||||
|
||||
[B(Name = "redirect_uris")]
|
||||
[J("redirect_uris")]
|
||||
public string RedirectUrisInternal {
|
||||
set => RedirectUris = value.Split('\n').ToList();
|
||||
get => string.Join('\n', RedirectUris);
|
||||
}
|
||||
}
|
||||
|
||||
public class RegisterAppResponse {
|
||||
public required OauthApp App;
|
||||
|
||||
[J("id")] public string Id => App.Id;
|
||||
[J("name")] public string Name => App.Name;
|
||||
[J("website")] public string? Website => App.Website;
|
||||
[J("client_id")] public string ClientId => App.ClientId;
|
||||
[J("client_secret")] public string ClientSecret => App.ClientSecret;
|
||||
[J("redirect_uri")] public string RedirectUri => string.Join("\n", App.RedirectUris);
|
||||
|
||||
[J("vapid_key")] public required string? VapidKey { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas;
|
||||
|
||||
public class MastodonErrorResponse {
|
||||
[J("error")] public required string Error { get; set; }
|
||||
[J("error_description")] public required string? Description { get; set; }
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Extensions;
|
||||
|
||||
public static class ModelBinderProviderExtensions {
|
||||
public static void AddHybridBindingProvider(this IList<IModelBinderProvider> providers) {
|
||||
if (providers.Single(provider => provider.GetType() == typeof(BodyModelBinderProvider)) is not
|
||||
BodyModelBinderProvider bodyProvider ||
|
||||
providers.Single(provider => provider.GetType() == typeof(ComplexObjectModelBinderProvider)) is not
|
||||
ComplexObjectModelBinderProvider complexProvider)
|
||||
throw new Exception("Failed to set up hybrid model binding provider");
|
||||
|
||||
var hybridProvider = new HybridModelBinderProvider(bodyProvider, complexProvider);
|
||||
|
||||
providers.Insert(0, hybridProvider);
|
||||
}
|
||||
}
|
||||
|
||||
public class HybridModelBinderProvider(
|
||||
IModelBinderProvider bodyProvider,
|
||||
IModelBinderProvider complexProvider) : IModelBinderProvider {
|
||||
public IModelBinder? GetBinder(ModelBinderProviderContext context) {
|
||||
if (context.BindingInfo.BindingSource == null) return null;
|
||||
if (!context.BindingInfo.BindingSource.CanAcceptDataFrom(HybridBindingSource.Hybrid)) return null;
|
||||
|
||||
context.BindingInfo.BindingSource = BindingSource.Body;
|
||||
var bodyBinder = bodyProvider.GetBinder(context);
|
||||
context.BindingInfo.BindingSource = BindingSource.ModelBinding;
|
||||
var complexBinder = complexProvider.GetBinder(context);
|
||||
|
||||
return new HybridModelBinder(bodyBinder, complexBinder);
|
||||
}
|
||||
}
|
||||
|
||||
public class HybridModelBinder(
|
||||
IModelBinder? bodyBinder,
|
||||
IModelBinder? complexBinder
|
||||
) : IModelBinder {
|
||||
public async Task BindModelAsync(ModelBindingContext bindingContext) {
|
||||
if (bodyBinder != null && bindingContext is
|
||||
{ IsTopLevelObject: true, HttpContext.Request.HasFormContentType: false }) {
|
||||
bindingContext.BindingSource = BindingSource.Body;
|
||||
await bodyBinder.BindModelAsync(bindingContext);
|
||||
}
|
||||
|
||||
if (complexBinder != null && !bindingContext.Result.IsModelSet) {
|
||||
bindingContext.BindingSource = BindingSource.ModelBinding;
|
||||
await complexBinder.BindModelAsync(bindingContext);
|
||||
}
|
||||
|
||||
if (bindingContext.Result.IsModelSet) bindingContext.Model = bindingContext.Result.Model;
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
|
||||
public class FromHybridAttribute : Attribute, IBindingSourceMetadata {
|
||||
public BindingSource BindingSource => HybridBindingSource.Hybrid;
|
||||
}
|
||||
|
||||
public sealed class HybridBindingSource() : BindingSource("Hybrid", "Hybrid", true, true) {
|
||||
public static readonly HybridBindingSource Hybrid = new();
|
||||
|
||||
public override bool CanAcceptDataFrom(BindingSource bindingSource) {
|
||||
return bindingSource == this;
|
||||
}
|
||||
}
|
|
@ -107,11 +107,16 @@ public static class ServiceExtensions {
|
|||
Type = SecuritySchemeType.Http,
|
||||
Scheme = "bearer"
|
||||
});
|
||||
options.AddSecurityDefinition("mastodon", new OpenApiSecurityScheme {
|
||||
Name = "Authorization token",
|
||||
In = ParameterLocation.Header,
|
||||
Type = SecuritySchemeType.Http,
|
||||
Scheme = "bearer"
|
||||
});
|
||||
options.OperationFilter<AuthorizeCheckOperationFilter>();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static void AddSlidingWindowRateLimiter(this IServiceCollection services) {
|
||||
//TODO: separate limiter for authenticated users, partitioned by user id
|
||||
//TODO: ipv6 /64 subnet buckets
|
||||
|
@ -153,16 +158,22 @@ public static class ServiceExtensions {
|
|||
return;
|
||||
|
||||
//TODO: separate admin & user authorize attributes
|
||||
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||
var hasAuthenticate = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||
.OfType<AuthenticateAttribute>().Any() ||
|
||||
context.MethodInfo.GetCustomAttributes(true)
|
||||
.OfType<AuthenticateAttribute>().Any();
|
||||
|
||||
var hasOauthAuthenticate = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
|
||||
.OfType<AuthenticateOauthAttribute>().Any() ||
|
||||
context.MethodInfo.GetCustomAttributes(true)
|
||||
.OfType<AuthenticateOauthAttribute>().Any();
|
||||
|
||||
if (!hasAuthorize) return;
|
||||
if (!hasAuthenticate && !hasOauthAuthenticate) return;
|
||||
|
||||
var schema = new OpenApiSecurityScheme {
|
||||
Reference = new OpenApiReference {
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "user"
|
||||
Id = hasAuthenticate ? "user" : "mastodon"
|
||||
}
|
||||
};
|
||||
|
||||
|
|
34
Iceshrimp.Backend/Core/Helpers/JsonConverters.cs
Normal file
34
Iceshrimp.Backend/Core/Helpers/JsonConverters.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Helpers;
|
||||
|
||||
public class EnsureArrayConverter : JsonConverter<List<string>> {
|
||||
public override List<string> Read(ref Utf8JsonReader reader, Type typeToConvert,
|
||||
JsonSerializerOptions options) {
|
||||
if (reader.TokenType == JsonTokenType.StartArray) {
|
||||
var list = new List<string>();
|
||||
reader.Read();
|
||||
|
||||
while (reader.TokenType != JsonTokenType.EndArray) {
|
||||
list.Add(JsonSerializer.Deserialize<string>(ref reader, options) ??
|
||||
throw new InvalidOperationException());
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.String) {
|
||||
var str = JsonSerializer.Deserialize<string>(ref reader, options) ??
|
||||
throw new InvalidOperationException();
|
||||
return [str];
|
||||
}
|
||||
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, List<string> value, JsonSerializerOptions options) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
74
Iceshrimp.Backend/Core/Helpers/MastodonOauthHelpers.cs
Normal file
74
Iceshrimp.Backend/Core/Helpers/MastodonOauthHelpers.cs
Normal file
|
@ -0,0 +1,74 @@
|
|||
using Iceshrimp.Backend.Core.Middleware;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Helpers;
|
||||
|
||||
public static class MastodonOauthHelpers {
|
||||
private static readonly List<string> ReadScopes = [
|
||||
"read:accounts",
|
||||
"read:blocks",
|
||||
"read:bookmarks",
|
||||
"read:favourites",
|
||||
"read:filters",
|
||||
"read:follows",
|
||||
"read:lists",
|
||||
"read:mutes",
|
||||
"read:notifications",
|
||||
"read:search",
|
||||
"read:statuses"
|
||||
];
|
||||
|
||||
private static readonly List<string> WriteScopes = [
|
||||
"write:accounts",
|
||||
"write:blocks",
|
||||
"write:bookmarks",
|
||||
"write:conversations",
|
||||
"write:favourites",
|
||||
"write:filters",
|
||||
"write:follows",
|
||||
"write:lists",
|
||||
"write:media",
|
||||
"write:mutes",
|
||||
"write:notifications",
|
||||
"write:reports",
|
||||
"write:statuses"
|
||||
];
|
||||
|
||||
private static readonly List<string> FollowScopes = [
|
||||
"read:follows",
|
||||
"read:blocks",
|
||||
"read:mutes",
|
||||
"write:follows",
|
||||
"write:blocks",
|
||||
"write:mutes"
|
||||
];
|
||||
|
||||
public static IEnumerable<string> ExpandScopes(IEnumerable<string> scopes) {
|
||||
var res = new List<string>();
|
||||
foreach (var scope in scopes) {
|
||||
if (scope == "read")
|
||||
res.AddRange(ReadScopes);
|
||||
if (scope == "write")
|
||||
res.AddRange(WriteScopes);
|
||||
if (scope == "follow")
|
||||
res.AddRange(FollowScopes);
|
||||
else {
|
||||
res.Add(scope);
|
||||
}
|
||||
}
|
||||
|
||||
return res.Distinct();
|
||||
}
|
||||
|
||||
private static readonly List<string> ForbiddenSchemes = ["javascript", "file", "data", "mailto", "tel"];
|
||||
|
||||
public static bool ValidateRedirectUri(string uri) {
|
||||
if (uri == "urn:ietf:wg:oauth:2.0:oob") return true;
|
||||
try {
|
||||
var proto = new Uri(uri).Scheme;
|
||||
return !ForbiddenSchemes.Contains(proto);
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware {
|
|||
}
|
||||
|
||||
var token = header[7..];
|
||||
var session = await db.Sessions.Include(p => p.User).FirstOrDefaultAsync(p => p.Token == token);
|
||||
var session = await db.Sessions.Include(p => p.User).FirstOrDefaultAsync(p => p.Token == token && p.Active);
|
||||
if (session == null) {
|
||||
await next(ctx);
|
||||
return;
|
||||
|
@ -34,7 +34,7 @@ public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware {
|
|||
|
||||
public class AuthenticateAttribute : Attribute;
|
||||
|
||||
public static class HttpContextExtensions {
|
||||
public static partial class HttpContextExtensions {
|
||||
private const string Key = "session";
|
||||
|
||||
internal static void SetSession(this HttpContext ctx, Session session) {
|
||||
|
|
|
@ -85,6 +85,14 @@ public class GracefulException(HttpStatusCode statusCode, string error, string m
|
|||
public static GracefulException NotFound(string message, string? details = null) {
|
||||
return new GracefulException(HttpStatusCode.NotFound, message, details);
|
||||
}
|
||||
|
||||
public static GracefulException BadRequest(string message, string? details = null) {
|
||||
return new GracefulException(HttpStatusCode.BadRequest, message, details);
|
||||
}
|
||||
|
||||
public static GracefulException RecordNotFound() {
|
||||
return new GracefulException(HttpStatusCode.NotFound, "Record not found");
|
||||
}
|
||||
}
|
||||
|
||||
public enum ExceptionVerbosity {
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Helpers;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Middleware;
|
||||
|
||||
public class OauthAuthenticationMiddleware(DatabaseContext db) : IMiddleware {
|
||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
||||
var endpoint = ctx.Features.Get<IEndpointFeature>()?.Endpoint;
|
||||
var attribute = endpoint?.Metadata.GetMetadata<AuthenticateOauthAttribute>();
|
||||
|
||||
if (attribute != null) {
|
||||
var request = ctx.Request;
|
||||
var header = request.Headers.Authorization.ToString();
|
||||
if (!header.ToLowerInvariant().StartsWith("bearer ")) {
|
||||
await next(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
header = header[7..];
|
||||
var token = await db.OauthTokens
|
||||
.Include(p => p.User)
|
||||
.Include(p => p.App)
|
||||
.FirstOrDefaultAsync(p => p.Token == header && p.Active);
|
||||
|
||||
if (token == null) {
|
||||
await next(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (attribute.Scopes.Length > 0 &&
|
||||
attribute.Scopes.Except(MastodonOauthHelpers.ExpandScopes(token.Scopes)).Any()) {
|
||||
await next(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.SetOauthToken(token);
|
||||
}
|
||||
|
||||
await next(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthenticateOauthAttribute(params string[] scopes) : Attribute {
|
||||
public string[] Scopes = scopes;
|
||||
}
|
||||
|
||||
public static partial class HttpContextExtensions {
|
||||
private const string MastodonKey = "masto-session";
|
||||
|
||||
internal static void SetOauthToken(this HttpContext ctx, OauthToken session) {
|
||||
ctx.Items.Add(MastodonKey, session);
|
||||
}
|
||||
|
||||
public static OauthToken? GetOauthToken(this HttpContext ctx) {
|
||||
ctx.Items.TryGetValue(MastodonKey, out var session);
|
||||
return session as OauthToken;
|
||||
}
|
||||
|
||||
public static User? GetOauthUser(this HttpContext ctx) {
|
||||
ctx.Items.TryGetValue(MastodonKey, out var session);
|
||||
return (session as OauthToken)?.User;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Middleware;
|
||||
|
||||
public class OauthAuthorizationMiddleware : IMiddleware {
|
||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) {
|
||||
var endpoint = ctx.Features.Get<IEndpointFeature>()?.Endpoint;
|
||||
var attribute = endpoint?.Metadata.GetMetadata<AuthorizeOauthAttribute>();
|
||||
|
||||
if (attribute != null)
|
||||
if (ctx.GetOauthToken() is not { Active: true })
|
||||
throw GracefulException.Forbidden("This method requires an authenticated user");
|
||||
|
||||
await next(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthorizeOauthAttribute : Attribute;
|
|
@ -14,41 +14,41 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.1.0"/>
|
||||
<PackageReference Include="Asp.Versioning.Http" Version="8.0.0"/>
|
||||
<PackageReference Include="cuid.net" Version="5.0.2"/>
|
||||
<PackageReference Include="dotNetRdf.Core" Version="3.2.1-dev"/>
|
||||
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="8.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0"/>
|
||||
<PackageReference Include="AngleSharp" Version="1.1.0" />
|
||||
<PackageReference Include="Asp.Versioning.Http" Version="8.0.0" />
|
||||
<PackageReference Include="cuid.net" Version="5.0.2" />
|
||||
<PackageReference Include="dotNetRdf.Core" Version="3.2.1-dev" />
|
||||
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0"/>
|
||||
<PackageReference Include="protobuf-net" Version="3.2.30"/>
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.7.17"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/>
|
||||
<PackageReference Include="Vite.AspNetCore" Version="1.11.0"/>
|
||||
<PackageReference Include="YamlDotNet" Version="13.7.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.30" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.7.17" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Vite.AspNetCore" Version="1.11.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="13.7.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Pages\Error.cshtml"/>
|
||||
<AdditionalFiles Include="Pages\Shared\_Layout.cshtml"/>
|
||||
<AdditionalFiles Include="Pages\_ViewImports.cshtml"/>
|
||||
<AdditionalFiles Include="Pages\_ViewStart.cshtml"/>
|
||||
<AdditionalFiles Include="Pages\Error.cshtml" />
|
||||
<AdditionalFiles Include="Pages\Shared\_Layout.cshtml" />
|
||||
<AdditionalFiles Include="Pages\_ViewImports.cshtml" />
|
||||
<AdditionalFiles Include="Pages\_ViewStart.cshtml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Core\Database\Tables\"/>
|
||||
<Folder Include="Core\Database\Tables\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="wwwroot\.vite\manifest.json" CopyToPublishDirectory="PreserveNewest"/>
|
||||
<Content Include="wwwroot\.vite\manifest.json" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
using Asp.Versioning;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerUI;
|
||||
using Vite.AspNetCore.Extensions;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
@ -14,7 +12,7 @@ builder.Configuration
|
|||
.AddIniFile(Environment.GetEnvironmentVariable("ICESHRIMP_CONFIG_OVERRIDES") ?? "configuration.overrides.ini",
|
||||
true, true);
|
||||
|
||||
builder.Services.AddControllers()
|
||||
builder.Services.AddControllers(options => { options.ModelBinderProviders.AddHybridBindingProvider(); })
|
||||
.AddNewtonsoftJson() //TODO: remove once dotNetRdf switches to System.Text.Json
|
||||
.AddMultiFormatter();
|
||||
builder.Services.AddApiVersioning(options => {
|
||||
|
|
Loading…
Add table
Reference in a new issue