Compare commits
7 commits
9160281ea2
...
c8cd483d20
Author | SHA1 | Date | |
---|---|---|---|
c8cd483d20 | |||
7cb17409cd | |||
4e9c4af4a5 | |||
142ff36d3a | |||
d87856bf2c | |||
6abf505c40 | |||
d0bf638a21 |
18 changed files with 237 additions and 201 deletions
|
@ -6,7 +6,6 @@ using Foxnouns.Backend.Utils;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using FediverseAuthService = Foxnouns.Backend.Services.Auth.FediverseAuthService;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Authentication;
|
||||
|
||||
|
@ -23,9 +22,15 @@ public class FediverseAuthController(
|
|||
|
||||
[HttpGet]
|
||||
[ProducesResponseType<FediverseUrlResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetFediverseUrlAsync([FromQuery] string instance)
|
||||
public async Task<IActionResult> GetFediverseUrlAsync(
|
||||
[FromQuery] string instance,
|
||||
[FromQuery] bool forceRefresh = false
|
||||
)
|
||||
{
|
||||
var url = await fediverseAuthService.GenerateAuthUrlAsync(instance);
|
||||
if (instance.Any(c => c is '@' or ':' or '/') || !instance.Contains('.'))
|
||||
throw new ApiError.BadRequest("Not a valid domain.", "instance", instance);
|
||||
|
||||
var url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh);
|
||||
return Ok(new FediverseUrlResponse(url));
|
||||
}
|
||||
|
||||
|
@ -34,7 +39,11 @@ public class FediverseAuthController(
|
|||
public async Task<IActionResult> FediverseCallbackAsync([FromBody] CallbackRequest req)
|
||||
{
|
||||
var app = await fediverseAuthService.GetApplicationAsync(req.Instance);
|
||||
var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code);
|
||||
var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync(
|
||||
app,
|
||||
req.Code,
|
||||
req.State
|
||||
);
|
||||
|
||||
var user = await authService.AuthenticateUserAsync(
|
||||
AuthType.Fediverse,
|
||||
|
@ -70,12 +79,16 @@ public class FediverseAuthController(
|
|||
)
|
||||
{
|
||||
var ticketData = await keyCacheService.GetKeyAsync<FediverseTicketData>(
|
||||
$"fediverse:{req.Ticket}"
|
||||
$"fediverse:{req.Ticket}",
|
||||
delete: true
|
||||
);
|
||||
if (ticketData == null)
|
||||
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
|
||||
|
||||
var app = await db.FediverseApplications.FindAsync(ticketData.ApplicationId);
|
||||
if (app == null)
|
||||
throw new FoxnounsError("Null application found for ticket");
|
||||
|
||||
if (
|
||||
await db.AuthMethods.AnyAsync(a =>
|
||||
a.AuthType == AuthType.Fediverse
|
||||
|
@ -105,7 +118,7 @@ public class FediverseAuthController(
|
|||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
}
|
||||
|
||||
public record CallbackRequest(string Instance, string Code);
|
||||
public record CallbackRequest(string Instance, string Code, string State);
|
||||
|
||||
private record FediverseUrlResponse(string Url);
|
||||
|
||||
|
|
|
@ -94,6 +94,7 @@ public class MembersController(
|
|||
Names = req.Names ?? [],
|
||||
Pronouns = req.Pronouns ?? [],
|
||||
Unlisted = req.Unlisted ?? false,
|
||||
Sid = null!,
|
||||
};
|
||||
db.Add(member);
|
||||
|
||||
|
|
52
Foxnouns.Backend/Controllers/SidController.cs
Normal file
52
Foxnouns.Backend/Controllers/SidController.cs
Normal file
|
@ -0,0 +1,52 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/sid")]
|
||||
[SuppressMessage(
|
||||
"Performance",
|
||||
"CA1862:Use the \'StringComparison\' method overloads to perform case-insensitive string comparisons",
|
||||
Justification = "Not usable with EFCore"
|
||||
)]
|
||||
public class SidController(Config config, DatabaseContext db) : ApiControllerBase
|
||||
{
|
||||
[HttpGet("{**id}")]
|
||||
public async Task<IActionResult> ResolveSidAsync(string id, CancellationToken ct = default) =>
|
||||
id.Length switch
|
||||
{
|
||||
5 => await ResolveUserSidAsync(id, ct),
|
||||
6 => await ResolveMemberSidAsync(id, ct),
|
||||
_ => Redirect(config.BaseUrl),
|
||||
};
|
||||
|
||||
private async Task<IActionResult> ResolveUserSidAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
var username = await db
|
||||
.Users.Where(u => u.Sid == id.ToLowerInvariant() && !u.Deleted)
|
||||
.Select(u => u.Username)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
if (username == null)
|
||||
return Redirect(config.BaseUrl);
|
||||
|
||||
return Redirect($"{config.BaseUrl}/@{username}");
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ResolveMemberSidAsync(
|
||||
string id,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
var member = await db
|
||||
.Members.Include(m => m.User)
|
||||
.Where(m => m.Sid == id.ToLowerInvariant() && !m.User.Deleted)
|
||||
.Select(m => new { m.Name, m.User.Username })
|
||||
.FirstOrDefaultAsync(ct);
|
||||
if (member == null)
|
||||
return Redirect(config.BaseUrl);
|
||||
|
||||
return Redirect($"{config.BaseUrl}/@{member.Username}/{member.Name}");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241123210306_RemoveFediverseApplicationTokens")]
|
||||
public partial class RemoveFediverseApplicationTokens : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "access_token", table: "fediverse_applications");
|
||||
|
||||
migrationBuilder.DropColumn(name: "token_valid_until", table: "fediverse_applications");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "access_token",
|
||||
table: "fediverse_applications",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<Instant>(
|
||||
name: "token_valid_until",
|
||||
table: "fediverse_applications",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -107,10 +107,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("access_token");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
|
@ -130,10 +126,6 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("integer")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.Property<Instant?>("TokenValidUntil")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("token_valid_until");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_fediverse_applications");
|
||||
|
||||
|
|
|
@ -8,10 +8,6 @@ public class FediverseApplication : BaseModel
|
|||
public required string ClientId { get; set; }
|
||||
public required string ClientSecret { get; set; }
|
||||
public required FediverseInstanceType InstanceType { get; set; }
|
||||
|
||||
// These are for ensuring the application is still valid.
|
||||
public string? AccessToken { get; set; }
|
||||
public Instant? TokenValidUntil { get; set; }
|
||||
}
|
||||
|
||||
public enum FediverseInstanceType
|
||||
|
|
|
@ -10,7 +10,7 @@ using SixLabors.ImageSharp.Processing.Processors.Transforms;
|
|||
|
||||
namespace Foxnouns.Backend.Extensions;
|
||||
|
||||
public static class AvatarObjectExtensions
|
||||
public static class ImageObjectExtensions
|
||||
{
|
||||
private static readonly string[] ValidContentTypes = ["image/png", "image/webp", "image/jpeg"];
|
||||
|
||||
|
@ -39,7 +39,7 @@ public static class AvatarObjectExtensions
|
|||
) => await objectStorageService.RemoveObjectAsync(CreateFlagInvocable.Path(hash), ct);
|
||||
|
||||
public static async Task<(string Hash, Stream Image)> ConvertBase64UriToImage(
|
||||
this string uri,
|
||||
string uri,
|
||||
int size,
|
||||
bool crop
|
||||
)
|
|
@ -20,6 +20,7 @@
|
|||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0"/>
|
||||
<PackageReference Include="Minio" Version="6.0.3"/>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
<PackageReference Include="NodaTime" Version="3.1.11"/>
|
||||
|
@ -35,6 +36,7 @@
|
|||
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0"/>
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.0"/>
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -26,7 +26,8 @@ public class CreateFlagInvocable(
|
|||
|
||||
try
|
||||
{
|
||||
var (hash, image) = await Payload.ImageData.ConvertBase64UriToImage(
|
||||
var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage(
|
||||
Payload.ImageData,
|
||||
size: 256,
|
||||
crop: false
|
||||
);
|
||||
|
|
|
@ -39,7 +39,11 @@ public class MemberAvatarUpdateInvocable(
|
|||
|
||||
try
|
||||
{
|
||||
var (hash, image) = await newAvatar.ConvertBase64UriToImage(size: 512, crop: true);
|
||||
var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage(
|
||||
newAvatar,
|
||||
size: 512,
|
||||
crop: true
|
||||
);
|
||||
var prevHash = member.Avatar;
|
||||
|
||||
await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp");
|
||||
|
|
|
@ -39,7 +39,11 @@ public class UserAvatarUpdateInvocable(
|
|||
|
||||
try
|
||||
{
|
||||
var (hash, image) = await newAvatar.ConvertBase64UriToImage(size: 512, crop: true);
|
||||
var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage(
|
||||
newAvatar,
|
||||
size: 512,
|
||||
crop: true
|
||||
);
|
||||
image.Seek(0, SeekOrigin.Begin);
|
||||
var prevHash = user.Avatar;
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ builder.AddSerilog();
|
|||
builder
|
||||
.WebHost.UseSentry(opts =>
|
||||
{
|
||||
opts.Dsn = config.Logging.SentryUrl;
|
||||
opts.Dsn = config.Logging.SentryUrl ?? "";
|
||||
opts.TracesSampleRate = config.Logging.SentryTracesSampleRate;
|
||||
opts.MaxRequestBodySize = RequestSize.Small;
|
||||
})
|
||||
|
|
|
@ -45,6 +45,7 @@ public class AuthService(
|
|||
},
|
||||
},
|
||||
LastActive = clock.GetCurrentInstant(),
|
||||
Sid = null!,
|
||||
};
|
||||
|
||||
db.Add(user);
|
||||
|
@ -88,6 +89,7 @@ public class AuthService(
|
|||
},
|
||||
},
|
||||
LastActive = clock.GetCurrentInstant(),
|
||||
Sid = null!,
|
||||
};
|
||||
|
||||
db.Add(user);
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Web;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Duration = NodaTime.Duration;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||
|
||||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
@ -17,16 +18,13 @@ public partial class FediverseAuthService
|
|||
Snowflake? existingAppId = null
|
||||
)
|
||||
{
|
||||
var resp = await _client.PostAsync(
|
||||
var resp = await _client.PostAsJsonAsync(
|
||||
$"https://{instance}/api/v1/apps",
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "client_name", $"pronouns.cc (+{_config.BaseUrl})" },
|
||||
{ "redirect_uris", MastodonRedirectUri(instance) },
|
||||
{ "scope", "read:accounts" },
|
||||
{ "website", _config.BaseUrl },
|
||||
}
|
||||
new CreateMastodonApplicationRequest(
|
||||
ClientName: $"pronouns.cc (+{_config.BaseUrl})",
|
||||
RedirectUris: MastodonRedirectUri(instance),
|
||||
Scopes: "read read:accounts",
|
||||
Website: _config.BaseUrl
|
||||
)
|
||||
);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
@ -37,12 +35,6 @@ public partial class FediverseAuthService
|
|||
$"Application created on Mastodon-compatible instance {instance} was null"
|
||||
);
|
||||
|
||||
var token = await GetMastodonAppTokenAsync(
|
||||
instance,
|
||||
mastodonApp.ClientId,
|
||||
mastodonApp.ClientSecret
|
||||
);
|
||||
|
||||
FediverseApplication app;
|
||||
|
||||
if (existingAppId == null)
|
||||
|
@ -54,8 +46,6 @@ public partial class FediverseAuthService
|
|||
ClientSecret = mastodonApp.ClientSecret,
|
||||
Domain = instance,
|
||||
InstanceType = FediverseInstanceType.MastodonApi,
|
||||
AccessToken = token,
|
||||
TokenValidUntil = _clock.GetCurrentInstant() + Duration.FromDays(60),
|
||||
};
|
||||
|
||||
_db.Add(app);
|
||||
|
@ -69,8 +59,6 @@ public partial class FediverseAuthService
|
|||
app.ClientId = mastodonApp.ClientId;
|
||||
app.ClientSecret = mastodonApp.ClientSecret;
|
||||
app.InstanceType = FediverseInstanceType.MastodonApi;
|
||||
app.AccessToken = null;
|
||||
app.TokenValidUntil = null;
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
@ -78,8 +66,14 @@ public partial class FediverseAuthService
|
|||
return app;
|
||||
}
|
||||
|
||||
private async Task<FediverseUser> GetMastodonUserAsync(FediverseApplication app, string code)
|
||||
private async Task<FediverseUser> GetMastodonUserAsync(
|
||||
FediverseApplication app,
|
||||
string code,
|
||||
string state
|
||||
)
|
||||
{
|
||||
await _keyCacheService.ValidateAuthStateAsync(state);
|
||||
|
||||
var tokenResp = await _client.PostAsync(
|
||||
MastodonTokenUri(app.Domain),
|
||||
new FormUrlEncodedContent(
|
||||
|
@ -124,109 +118,27 @@ public partial class FediverseAuthService
|
|||
|
||||
private record MastodonTokenResponse([property: J("access_token")] string AccessToken);
|
||||
|
||||
// TODO: Mastodon's OAuth documentation doesn't specify a "state" parameter. that feels... wrong
|
||||
// https://docs.joinmastodon.org/methods/oauth/
|
||||
private async Task<string> GenerateMastodonAuthUrlAsync(FediverseApplication app)
|
||||
private async Task<string> GenerateMastodonAuthUrlAsync(
|
||||
FediverseApplication app,
|
||||
bool forceRefresh
|
||||
)
|
||||
{
|
||||
try
|
||||
if (forceRefresh)
|
||||
{
|
||||
await ValidateMastodonAppAsync(app);
|
||||
}
|
||||
catch (FoxnounsError.RemoteAuthError e)
|
||||
{
|
||||
_logger.Error(
|
||||
e,
|
||||
"Error validating app token for {AppId} on {Instance}",
|
||||
app.Id,
|
||||
app.Domain
|
||||
_logger.Information(
|
||||
"An app credentials refresh was requested for {ApplicationId}, creating a new application",
|
||||
app.Id
|
||||
);
|
||||
|
||||
app = await CreateMastodonApplicationAsync(app.Domain, existingAppId: app.Id);
|
||||
}
|
||||
|
||||
var state = HttpUtility.UrlEncode(await _keyCacheService.GenerateAuthStateAsync());
|
||||
|
||||
return $"https://{app.Domain}/oauth/authorize?response_type=code"
|
||||
+ $"&client_id={app.ClientId}"
|
||||
+ $"&scope={HttpUtility.UrlEncode("read:accounts")}"
|
||||
+ $"&redirect_uri={HttpUtility.UrlEncode(MastodonRedirectUri(app.Domain))}";
|
||||
}
|
||||
|
||||
private async Task ValidateMastodonAppAsync(FediverseApplication app)
|
||||
{
|
||||
// If we don't have an access token stored, or it's too old, get one
|
||||
// When doing this we don't need to fetch the application info
|
||||
if (app.AccessToken == null || app.TokenValidUntil < _clock.GetCurrentInstant())
|
||||
{
|
||||
_logger.Debug(
|
||||
"Application {AppId} on instance {Instance} has no valid token, fetching it",
|
||||
app.Id,
|
||||
app.Domain
|
||||
);
|
||||
|
||||
app.AccessToken = await GetMastodonAppTokenAsync(
|
||||
app.Domain,
|
||||
app.ClientId,
|
||||
app.ClientSecret
|
||||
);
|
||||
app.TokenValidUntil = _clock.GetCurrentInstant() + Duration.FromDays(60);
|
||||
|
||||
_db.Update(app);
|
||||
await _db.SaveChangesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Debug(
|
||||
"Checking whether application {AppId} on instance {Instance} is still valid",
|
||||
app.Id,
|
||||
app.Domain
|
||||
);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, MastodonCurrentAppUri(app.Domain));
|
||||
req.Headers.Add("Authorization", $"Bearer {app.AccessToken}");
|
||||
|
||||
var resp = await _client.SendAsync(req);
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await resp.Content.ReadAsStringAsync();
|
||||
throw new FoxnounsError.RemoteAuthError(
|
||||
"Verifying app credentials returned an error",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GetMastodonAppTokenAsync(
|
||||
string instance,
|
||||
string clientId,
|
||||
string clientSecret
|
||||
)
|
||||
{
|
||||
var resp = await _client.PostAsync(
|
||||
MastodonTokenUri(instance),
|
||||
new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "grant_type", "client_credentials" },
|
||||
{ "client_id", clientId },
|
||||
{ "client_secret", clientSecret },
|
||||
}
|
||||
)
|
||||
);
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await resp.Content.ReadAsStringAsync();
|
||||
throw new FoxnounsError.RemoteAuthError(
|
||||
"Requesting app token returned an error",
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
var token = (await resp.Content.ReadFromJsonAsync<MastodonTokenResponse>())?.AccessToken;
|
||||
if (token == null)
|
||||
{
|
||||
throw new FoxnounsError($"Token response from instance {instance} was invalid");
|
||||
}
|
||||
|
||||
return token;
|
||||
+ $"&redirect_uri={HttpUtility.UrlEncode(MastodonRedirectUri(app.Domain))}"
|
||||
+ $"&state={state}";
|
||||
}
|
||||
|
||||
private static string MastodonTokenUri(string instance) => $"https://{instance}/oauth/token";
|
||||
|
@ -234,12 +146,17 @@ public partial class FediverseAuthService
|
|||
private static string MastodonCurrentUserUri(string instance) =>
|
||||
$"https://{instance}/api/v1/accounts/verify_credentials";
|
||||
|
||||
private static string MastodonCurrentAppUri(string instance) =>
|
||||
$"https://{instance}/api/v1/apps/verify_credentials";
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")]
|
||||
private record PartialMastodonApplication(
|
||||
[property: J("name")] string Name,
|
||||
[property: J("client_id")] string ClientId,
|
||||
[property: J("client_secret")] string ClientSecret
|
||||
);
|
||||
|
||||
private record CreateMastodonApplicationRequest(
|
||||
[property: J("client_name")] string ClientName,
|
||||
[property: J("redirect_uris")] string RedirectUris,
|
||||
[property: J("scopes")] string Scopes,
|
||||
[property: J("website")] string Website
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||
|
||||
namespace Foxnouns.Backend.Services.Auth;
|
||||
|
@ -10,26 +9,27 @@ public partial class FediverseAuthService
|
|||
{
|
||||
private const string NodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/2.0";
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly HttpClient _client;
|
||||
private readonly DatabaseContext _db;
|
||||
private readonly ILogger _logger;
|
||||
private readonly Config _config;
|
||||
private readonly DatabaseContext _db;
|
||||
private readonly KeyCacheService _keyCacheService;
|
||||
private readonly ISnowflakeGenerator _snowflakeGenerator;
|
||||
private readonly IClock _clock;
|
||||
|
||||
public FediverseAuthService(
|
||||
ILogger logger,
|
||||
Config config,
|
||||
DatabaseContext db,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
IClock clock
|
||||
KeyCacheService keyCacheService,
|
||||
ISnowflakeGenerator snowflakeGenerator
|
||||
)
|
||||
{
|
||||
_logger = logger.ForContext<FediverseAuthService>();
|
||||
_config = config;
|
||||
_db = db;
|
||||
_keyCacheService = keyCacheService;
|
||||
_snowflakeGenerator = snowflakeGenerator;
|
||||
_clock = clock;
|
||||
_logger = logger.ForContext<FediverseAuthService>();
|
||||
|
||||
_client = new HttpClient();
|
||||
_client.DefaultRequestHeaders.Remove("User-Agent");
|
||||
_client.DefaultRequestHeaders.Remove("Accept");
|
||||
|
@ -37,10 +37,10 @@ public partial class FediverseAuthService
|
|||
_client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAuthUrlAsync(string instance)
|
||||
public async Task<string> GenerateAuthUrlAsync(string instance, bool forceRefresh)
|
||||
{
|
||||
var app = await GetApplicationAsync(instance);
|
||||
return await GenerateAuthUrlAsync(app);
|
||||
return await GenerateAuthUrlAsync(app, forceRefresh);
|
||||
}
|
||||
|
||||
// thank you, gargron and syuilo, for agreeing on a name for *once* in your lives,
|
||||
|
@ -96,21 +96,25 @@ public partial class FediverseAuthService
|
|||
);
|
||||
}
|
||||
|
||||
private async Task<string> GenerateAuthUrlAsync(FediverseApplication app) =>
|
||||
private async Task<string> GenerateAuthUrlAsync(FediverseApplication app, bool forceRefresh) =>
|
||||
app.InstanceType switch
|
||||
{
|
||||
FediverseInstanceType.MastodonApi => await GenerateMastodonAuthUrlAsync(app),
|
||||
FediverseInstanceType.MastodonApi => await GenerateMastodonAuthUrlAsync(
|
||||
app,
|
||||
forceRefresh
|
||||
),
|
||||
FediverseInstanceType.MisskeyApi => throw new NotImplementedException(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null),
|
||||
};
|
||||
|
||||
public async Task<FediverseUser> GetRemoteFediverseUserAsync(
|
||||
FediverseApplication app,
|
||||
string code
|
||||
string code,
|
||||
string state
|
||||
) =>
|
||||
app.InstanceType switch
|
||||
{
|
||||
FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code),
|
||||
FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code, state),
|
||||
FediverseInstanceType.MisskeyApi => throw new NotImplementedException(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null),
|
||||
};
|
||||
|
|
|
@ -32,7 +32,7 @@ public class MemberRendererService(DatabaseContext db, Config config)
|
|||
member.Id,
|
||||
member.Sid,
|
||||
member.Name,
|
||||
member.DisplayName,
|
||||
member.DisplayName ?? member.Name,
|
||||
member.Bio,
|
||||
AvatarUrlFor(member),
|
||||
member.Links,
|
||||
|
@ -60,7 +60,7 @@ public class MemberRendererService(DatabaseContext db, Config config)
|
|||
member.Id,
|
||||
member.Sid,
|
||||
member.Name,
|
||||
member.DisplayName,
|
||||
member.DisplayName ?? member.Name,
|
||||
member.Bio,
|
||||
AvatarUrlFor(member),
|
||||
member.Names,
|
||||
|
@ -87,7 +87,7 @@ public class MemberRendererService(DatabaseContext db, Config config)
|
|||
Snowflake Id,
|
||||
string Sid,
|
||||
string Name,
|
||||
string? DisplayName,
|
||||
string DisplayName,
|
||||
string? Bio,
|
||||
string? AvatarUrl,
|
||||
IEnumerable<FieldEntry> Names,
|
||||
|
@ -99,7 +99,7 @@ public class MemberRendererService(DatabaseContext db, Config config)
|
|||
Snowflake Id,
|
||||
string Sid,
|
||||
string Name,
|
||||
string? DisplayName,
|
||||
string DisplayName,
|
||||
string? Bio,
|
||||
string? AvatarUrl,
|
||||
string[] Links,
|
||||
|
|
|
@ -162,6 +162,7 @@ public static partial class ValidationUtils
|
|||
}
|
||||
|
||||
public const int MaxBioLength = 1024;
|
||||
public const int MaxAvatarLength = 1_500_000;
|
||||
|
||||
public static ValidationError? ValidateBio(string? bio)
|
||||
{
|
||||
|
@ -183,7 +184,10 @@ public static partial class ValidationUtils
|
|||
return avatar?.Length switch
|
||||
{
|
||||
0 => ValidationError.GenericValidationError("Avatar cannot be empty", null),
|
||||
> 1_500_000 => ValidationError.GenericValidationError("Avatar is too large", null),
|
||||
> MaxAvatarLength => ValidationError.GenericValidationError(
|
||||
"Avatar is too large",
|
||||
null
|
||||
),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -96,6 +96,19 @@
|
|||
"Mono.TextTemplating": "2.2.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Memory": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "zbnPX/JQ0pETRSUG9fNPBvpIq42Aufvs15gGYyNIMhCun9yhmWihz0WgsI7bSDPjxWTKBf8oX/zv6v2uZ3W9OQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Caching.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Options": "9.0.0",
|
||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Minio": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.3, )",
|
||||
|
@ -246,6 +259,16 @@
|
|||
"Swashbuckle.AspNetCore.SwaggerUI": "6.6.2"
|
||||
}
|
||||
},
|
||||
"System.Text.Json": {
|
||||
"type": "Direct",
|
||||
"requested": "[9.0.0, )",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A==",
|
||||
"dependencies": {
|
||||
"System.IO.Pipelines": "9.0.0",
|
||||
"System.Text.Encodings.Web": "9.0.0"
|
||||
}
|
||||
},
|
||||
"System.Text.RegularExpressions": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.3.1, )",
|
||||
|
@ -412,22 +435,10 @@
|
|||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "FPWZAa9c0H4dvOj351iR1jkUIs4u9ykL4Bm592yhjDyO5lCoWd+TMAHx2EMbarzUvCvgjWjJIoC6//Q9kH6YhA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "7pqivmrZDzo1ADPkRwjy+8jtRKWRCPag9qPI+p7sgu7Q4QreWhcvbiWXsbhP+yY8XSiDvZpu2/LWdBv7PnmOpQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Caching.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Options": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration": {
|
||||
|
@ -465,8 +476,8 @@
|
|||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA=="
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
|
@ -542,10 +553,11 @@
|
|||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
|
||||
"System.Diagnostics.DiagnosticSource": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Configuration": {
|
||||
|
@ -570,11 +582,11 @@
|
|||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
|
||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": {
|
||||
|
@ -591,8 +603,8 @@
|
|||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg=="
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
|
@ -983,8 +995,8 @@
|
|||
},
|
||||
"System.Diagnostics.DiagnosticSource": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ=="
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "ddppcFpnbohLWdYKr/ZeLZHmmI+DXFgZ3Snq+/E7SwcdW4UnvxmaugkwGywvGVWkHPGCSZjCP+MLzu23AL5SDw=="
|
||||
},
|
||||
"System.Diagnostics.Tracing": {
|
||||
"type": "Transitive",
|
||||
|
@ -1072,8 +1084,8 @@
|
|||
},
|
||||
"System.IO.Pipelines": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.3",
|
||||
"contentHash": "ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw=="
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "eA3cinogwaNB4jdjQHOP3Z3EuyiDII7MT35jgtnsA4vkn0LUrrSHsU0nzHTzFzmaFYeKV7MYyMxOocFzsBHpTw=="
|
||||
},
|
||||
"System.Linq": {
|
||||
"type": "Transitive",
|
||||
|
@ -1484,16 +1496,8 @@
|
|||
},
|
||||
"System.Text.Encodings.Web": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
|
||||
},
|
||||
"System.Text.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.4",
|
||||
"contentHash": "bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==",
|
||||
"dependencies": {
|
||||
"System.Text.Encodings.Web": "8.0.0"
|
||||
}
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "e2hMgAErLbKyUUwt18qSBf9T5Y+SFAL3ZedM8fLupkVj8Rj2PZ9oxQ37XX2LF8fTO1wNIxvKpihD7Of7D/NxZw=="
|
||||
},
|
||||
"System.Threading": {
|
||||
"type": "Transitive",
|
||||
|
|
Loading…
Reference in a new issue