chat: add initial GuildsController
This commit is contained in:
parent
7b4cbd4fb7
commit
727f2f6ba2
23 changed files with 248 additions and 38 deletions
|
@ -2,3 +2,6 @@
|
||||||
|
|
||||||
# CS9113: Parameter is unread.
|
# CS9113: Parameter is unread.
|
||||||
dotnet_diagnostic.CS9113.severity = silent
|
dotnet_diagnostic.CS9113.severity = silent
|
||||||
|
|
||||||
|
# EntityFramework.ModelValidation.UnlimitedStringLength
|
||||||
|
resharper_entity_framework_model_validation_unlimited_string_length_highlighting=none
|
44
.gitignore
vendored
44
.gitignore
vendored
|
@ -1,3 +1,47 @@
|
||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
.version
|
.version
|
||||||
|
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
.idea/**/discord.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
|
47
Foxchat.Chat/Controllers/Api/GuildsController.cs
Normal file
47
Foxchat.Chat/Controllers/Api/GuildsController.cs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
using Foxchat.Chat.Database;
|
||||||
|
using Foxchat.Chat.Database.Models;
|
||||||
|
using Foxchat.Chat.Middleware;
|
||||||
|
using Foxchat.Chat.Services;
|
||||||
|
using Foxchat.Core.Models;
|
||||||
|
using Foxchat.Core.Models.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using ApiError = Foxchat.Core.ApiError;
|
||||||
|
|
||||||
|
namespace Foxchat.Chat.Controllers.Api;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("/_fox/chat/guilds")]
|
||||||
|
public class GuildsController(ILogger logger, ChatContext db, UserResolverService userResolverService) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateGuild([FromBody] GuildsApi.CreateGuildRequest req)
|
||||||
|
{
|
||||||
|
var (instance, sig) = HttpContext.GetSignatureOrThrow();
|
||||||
|
if (sig.UserId == null) throw new ApiError.IncomingFederationError("This endpoint requires a user ID.");
|
||||||
|
|
||||||
|
var user = await userResolverService.ResolveUserAsync(instance, sig.UserId);
|
||||||
|
|
||||||
|
var guild = new Guild
|
||||||
|
{
|
||||||
|
Name = req.Name,
|
||||||
|
Owner = user,
|
||||||
|
};
|
||||||
|
db.Add(guild);
|
||||||
|
guild.Users.Add(user);
|
||||||
|
var defaultChannel = new Channel
|
||||||
|
{
|
||||||
|
Guild = guild,
|
||||||
|
Name = "general"
|
||||||
|
};
|
||||||
|
db.Add(defaultChannel);
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(new Guilds.Guild(
|
||||||
|
guild.Id.ToString(),
|
||||||
|
guild.Name,
|
||||||
|
[user.Id.ToString()],
|
||||||
|
[new Channels.PartialChannel(defaultChannel.Id.ToString(), defaultChannel.Name)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ using ApiError = Foxchat.Core.ApiError;
|
||||||
namespace Foxchat.Chat.Controllers;
|
namespace Foxchat.Chat.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Unauthenticated]
|
[ServerUnauthenticated]
|
||||||
[Route("/_fox/chat/hello")]
|
[Route("/_fox/chat/hello")]
|
||||||
public class HelloController(
|
public class HelloController(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
@ -27,6 +27,8 @@ public class HelloController(
|
||||||
|
|
||||||
if (!HttpContext.ExtractRequestData(out var signature, out var domain, out var signatureData))
|
if (!HttpContext.ExtractRequestData(out var signature, out var domain, out var signatureData))
|
||||||
throw new ApiError.IncomingFederationError("This endpoint requires signed requests.");
|
throw new ApiError.IncomingFederationError("This endpoint requires signed requests.");
|
||||||
|
if (domain != req.Host)
|
||||||
|
throw new ApiError.IncomingFederationError("Host is invalid.");
|
||||||
|
|
||||||
if (!requestSigningService.VerifySignature(node.PublicKey, signature, signatureData))
|
if (!requestSigningService.VerifySignature(node.PublicKey, signature, signatureData))
|
||||||
throw new ApiError.IncomingFederationError("Signature is not valid.");
|
throw new ApiError.IncomingFederationError("Signature is not valid.");
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
using Foxchat.Core.Models;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
namespace Foxchat.Chat.Database.Models;
|
namespace Foxchat.Chat.Database.Models;
|
||||||
|
|
||||||
public class User : BaseModel
|
public class User : BaseModel
|
||||||
|
@ -8,6 +11,7 @@ public class User : BaseModel
|
||||||
public string Username { get; init; } = null!;
|
public string Username { get; init; } = null!;
|
||||||
|
|
||||||
public string? Avatar { get; set; }
|
public string? Avatar { get; set; }
|
||||||
|
public Instant LastFetchedAt { get; set; }
|
||||||
|
|
||||||
public List<Guild> Guilds { get; } = [];
|
public List<Guild> Guilds { get; } = [];
|
||||||
public List<Guild> OwnedGuilds { get; } = [];
|
public List<Guild> OwnedGuilds { get; } = [];
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
using Foxchat.Chat.Middleware;
|
using Foxchat.Chat.Middleware;
|
||||||
|
using Foxchat.Chat.Services;
|
||||||
|
using Foxchat.Core.Middleware;
|
||||||
|
|
||||||
namespace Foxchat.Chat.Extensions;
|
namespace Foxchat.Chat.Extensions;
|
||||||
|
|
||||||
|
@ -7,12 +9,20 @@ public static class WebApplicationExtensions
|
||||||
public static IServiceCollection AddCustomMiddleware(this IServiceCollection services)
|
public static IServiceCollection AddCustomMiddleware(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
return services
|
return services
|
||||||
.AddScoped<AuthenticationMiddleware>();
|
.AddScoped<ErrorHandlerMiddleware>()
|
||||||
|
.AddScoped<ServerAuthenticationMiddleware>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app)
|
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
return app
|
return app
|
||||||
.UseMiddleware<AuthenticationMiddleware>();
|
.UseMiddleware<ErrorHandlerMiddleware>()
|
||||||
|
.UseMiddleware<ServerAuthenticationMiddleware>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddChatServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
return services
|
||||||
|
.AddScoped<UserResolverService>();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,14 +7,14 @@ using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Foxchat.Chat.Middleware;
|
namespace Foxchat.Chat.Middleware;
|
||||||
|
|
||||||
public class AuthenticationMiddleware(ILogger logger, ChatContext db, RequestSigningService requestSigningService)
|
public class ServerAuthenticationMiddleware(ILogger logger, ChatContext db, RequestSigningService requestSigningService)
|
||||||
: IMiddleware
|
: IMiddleware
|
||||||
{
|
{
|
||||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
|
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
|
||||||
{
|
{
|
||||||
var endpoint = ctx.GetEndpoint();
|
var endpoint = ctx.GetEndpoint();
|
||||||
// Endpoints require server authentication by default, unless they have the [Unauthenticated] attribute.
|
// Endpoints require server authentication by default, unless they have the [Unauthenticated] attribute.
|
||||||
var metadata = endpoint?.Metadata.GetMetadata<UnauthenticatedAttribute>();
|
var metadata = endpoint?.Metadata.GetMetadata<ServerUnauthenticatedAttribute>();
|
||||||
if (metadata != null)
|
if (metadata != null)
|
||||||
{
|
{
|
||||||
await next(ctx);
|
await next(ctx);
|
||||||
|
@ -41,8 +41,11 @@ public class AuthenticationMiddleware(ILogger logger, ChatContext db, RequestSig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute to be put on controllers or methods to indicate that it does <i>not</i> require a signed request.
|
||||||
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||||
public class UnauthenticatedAttribute : Attribute;
|
public class ServerUnauthenticatedAttribute : Attribute;
|
||||||
|
|
||||||
public static class HttpContextExtensions
|
public static class HttpContextExtensions
|
||||||
{
|
{
|
|
@ -34,6 +34,7 @@ builder.Services
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddCoreServices<ChatContext>()
|
.AddCoreServices<ChatContext>()
|
||||||
|
.AddChatServices()
|
||||||
.AddCustomMiddleware()
|
.AddCustomMiddleware()
|
||||||
.AddEndpointsApiExplorer()
|
.AddEndpointsApiExplorer()
|
||||||
.AddSwaggerGen();
|
.AddSwaggerGen();
|
||||||
|
|
35
Foxchat.Chat/Services/UserResolverService.cs
Normal file
35
Foxchat.Chat/Services/UserResolverService.cs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
using Foxchat.Chat.Database;
|
||||||
|
using Foxchat.Chat.Database.Models;
|
||||||
|
using Foxchat.Core.Federation;
|
||||||
|
using Foxchat.Core.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Foxchat.Chat.Services;
|
||||||
|
|
||||||
|
public class UserResolverService(ILogger logger, ChatContext db, RequestSigningService requestSigningService)
|
||||||
|
{
|
||||||
|
public async Task<User> ResolveUserAsync(IdentityInstance instance, string userId)
|
||||||
|
{
|
||||||
|
var user = await db.Users.FirstOrDefaultAsync(u => u.InstanceId == instance.Id && u.RemoteUserId == userId);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
// TODO: update user if it's been long enough
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userResponse = await requestSigningService.RequestAsync<Users.User>(HttpMethod.Get, instance.Domain,
|
||||||
|
$"/_fox/ident/users/{userId}");
|
||||||
|
|
||||||
|
user = new User
|
||||||
|
{
|
||||||
|
Instance = instance,
|
||||||
|
Username = userResponse.Username,
|
||||||
|
RemoteUserId = userResponse.Id,
|
||||||
|
Avatar = userResponse.AvatarUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
db.Add(user);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Foxchat.Core.Federation;
|
using Foxchat.Core.Federation;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
@ -5,11 +6,12 @@ namespace Foxchat.Core.Extensions;
|
||||||
|
|
||||||
public static class HttpContextExtensions
|
public static class HttpContextExtensions
|
||||||
{
|
{
|
||||||
public static bool ExtractRequestData(this HttpContext ctx, out string signature, out string domain, out SignatureData data)
|
public static bool ExtractRequestData(this HttpContext ctx, [NotNullWhen(true)] out string? signature,
|
||||||
|
[NotNullWhen(true)] out string? domain, [NotNullWhen(true)] out SignatureData? data)
|
||||||
{
|
{
|
||||||
signature = string.Empty;
|
signature = null;
|
||||||
domain = string.Empty;
|
domain = null;
|
||||||
data = SignatureData.Empty;
|
data = null;
|
||||||
|
|
||||||
if (!ctx.Request.Headers.TryGetValue(RequestSigningService.SIGNATURE_HEADER, out var encodedSignature))
|
if (!ctx.Request.Headers.TryGetValue(RequestSigningService.SIGNATURE_HEADER, out var encodedSignature))
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -33,18 +33,17 @@ public partial class RequestSigningService(ILogger logger, IClock clock, IDataba
|
||||||
public bool VerifySignature(
|
public bool VerifySignature(
|
||||||
string publicKey, string encodedSignature, SignatureData data)
|
string publicKey, string encodedSignature, SignatureData data)
|
||||||
{
|
{
|
||||||
var rsa = RSA.Create();
|
if (data.Host != _config.Domain)
|
||||||
rsa.ImportFromPem(publicKey);
|
throw new ApiError.IncomingFederationError("Request is not for this instance");
|
||||||
|
|
||||||
var now = _clock.GetCurrentInstant();
|
var now = _clock.GetCurrentInstant();
|
||||||
if ((now + Duration.FromMinutes(1)) < data.Time)
|
if (now + Duration.FromMinutes(1) < data.Time)
|
||||||
{
|
|
||||||
throw new ApiError.IncomingFederationError("Request was made in the future");
|
throw new ApiError.IncomingFederationError("Request was made in the future");
|
||||||
}
|
if (now - Duration.FromMinutes(1) > data.Time)
|
||||||
else if ((now - Duration.FromMinutes(1)) > data.Time)
|
|
||||||
{
|
|
||||||
throw new ApiError.IncomingFederationError("Request was made too long ago");
|
throw new ApiError.IncomingFederationError("Request was made too long ago");
|
||||||
}
|
|
||||||
|
var rsa = RSA.Create();
|
||||||
|
rsa.ImportFromPem(publicKey);
|
||||||
|
|
||||||
var plaintext = GeneratePlaintext(data);
|
var plaintext = GeneratePlaintext(data);
|
||||||
var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
|
var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
|
||||||
|
@ -70,7 +69,9 @@ public partial class RequestSigningService(ILogger logger, IClock clock, IDataba
|
||||||
return $"{time}:{data.Host}:{data.RequestPath}:{contentLength}:{userId}";
|
return $"{time}:{data.Host}:{data.RequestPath}:{contentLength}:{userId}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly InstantPattern _pattern = InstantPattern.Create("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.GetCultureInfo("en-US"));
|
private static readonly InstantPattern _pattern =
|
||||||
|
InstantPattern.Create("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.GetCultureInfo("en-US"));
|
||||||
|
|
||||||
private static string FormatTime(Instant time) => _pattern.Format(time);
|
private static string FormatTime(Instant time) => _pattern.Format(time);
|
||||||
public static Instant ParseTime(string header) => _pattern.Parse(header).GetValueOrThrow();
|
public static Instant ParseTime(string header) => _pattern.Parse(header).GetValueOrThrow();
|
||||||
}
|
}
|
|
@ -1,11 +1,10 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Foxchat.Core;
|
|
||||||
using Foxchat.Core.Models.Http;
|
using Foxchat.Core.Models.Http;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using ApiError = Foxchat.Core.ApiError;
|
|
||||||
using HttpApiError = Foxchat.Core.Models.Http.ApiError;
|
using HttpApiError = Foxchat.Core.Models.Http.ApiError;
|
||||||
|
|
||||||
namespace Foxchat.Identity.Middleware;
|
namespace Foxchat.Core.Middleware;
|
||||||
|
|
||||||
public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware
|
public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware
|
||||||
{
|
{
|
||||||
|
@ -23,7 +22,8 @@ public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware
|
||||||
|
|
||||||
if (ctx.Response.HasStarted)
|
if (ctx.Response.HasStarted)
|
||||||
{
|
{
|
||||||
logger.Error(e, "Error in {ClassName} ({Path}) after response started being sent", typeName, ctx.Request.Path);
|
logger.Error(e, "Error in {ClassName} ({Path}) after response started being sent", typeName,
|
||||||
|
ctx.Request.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e is ApiError ae)
|
if (e is ApiError ae)
|
8
Foxchat.Core/Models/Channels.cs
Normal file
8
Foxchat.Core/Models/Channels.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
namespace Foxchat.Core.Models;
|
||||||
|
|
||||||
|
public static class Channels
|
||||||
|
{
|
||||||
|
public record Channel(string Id, string GuildId, string Name, string? Topic);
|
||||||
|
|
||||||
|
public record PartialChannel(string Id, string Name);
|
||||||
|
}
|
14
Foxchat.Core/Models/Guilds.cs
Normal file
14
Foxchat.Core/Models/Guilds.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Foxchat.Core.Models;
|
||||||
|
|
||||||
|
public static class Guilds
|
||||||
|
{
|
||||||
|
public record Guild(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
IEnumerable<string> OwnerIds,
|
||||||
|
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
IEnumerable<Channels.PartialChannel>? Channels
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
namespace Foxchat.Core.Models.Http;
|
namespace Foxchat.Core.Models.Http;
|
||||||
|
|
||||||
public static class Apps
|
public static class AppsApi
|
||||||
{
|
{
|
||||||
public record CreateRequest(string Name, string[] Scopes, string[] RedirectUris);
|
public record CreateRequest(string Name, string[] Scopes, string[] RedirectUris);
|
||||||
public record CreateResponse(Ulid Id, string ClientId, string ClientSecret, string Name, string[] Scopes, string[] RedirectUris);
|
public record CreateResponse(Ulid Id, string ClientId, string ClientSecret, string Name, string[] Scopes, string[] RedirectUris);
|
6
Foxchat.Core/Models/Http/GuildsApi.cs
Normal file
6
Foxchat.Core/Models/Http/GuildsApi.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Foxchat.Core.Models.Http;
|
||||||
|
|
||||||
|
public static class GuildsApi
|
||||||
|
{
|
||||||
|
public record CreateGuildRequest(string Name);
|
||||||
|
}
|
8
Foxchat.Core/Models/Users.cs
Normal file
8
Foxchat.Core/Models/Users.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
namespace Foxchat.Core.Models;
|
||||||
|
|
||||||
|
public static class Users
|
||||||
|
{
|
||||||
|
public record User(string Id, string Username, string Instance, string? AvatarUrl);
|
||||||
|
|
||||||
|
public record PartialUser(string Id, string Username, string Instance);
|
||||||
|
}
|
|
@ -9,12 +9,12 @@ using Microsoft.AspNetCore.Mvc;
|
||||||
namespace Foxchat.Identity.Controllers.Oauth;
|
namespace Foxchat.Identity.Controllers.Oauth;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authenticate]
|
[ClientAuthenticate]
|
||||||
[Route("/_fox/ident/oauth/apps")]
|
[Route("/_fox/ident/oauth/apps")]
|
||||||
public class AppsController(ILogger logger, IdentityContext db) : ControllerBase
|
public class AppsController(ILogger logger, IdentityContext db) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> CreateApplication([FromBody] Apps.CreateRequest req)
|
public async Task<IActionResult> CreateApplication([FromBody] AppsApi.CreateRequest req)
|
||||||
{
|
{
|
||||||
var app = Application.Create(req.Name, req.Scopes, req.RedirectUris);
|
var app = Application.Create(req.Name, req.Scopes, req.RedirectUris);
|
||||||
db.Add(app);
|
db.Add(app);
|
||||||
|
@ -22,7 +22,7 @@ public class AppsController(ILogger logger, IdentityContext db) : ControllerBase
|
||||||
|
|
||||||
logger.Information("Created new application {Name} with ID {Id} and client ID {ClientId}", app.Name, app.Id, app.ClientId);
|
logger.Information("Created new application {Name} with ID {Id} and client ID {ClientId}", app.Name, app.Id, app.ClientId);
|
||||||
|
|
||||||
return Ok(new Apps.CreateResponse(
|
return Ok(new AppsApi.CreateResponse(
|
||||||
app.Id, app.ClientId, app.ClientSecret, app.Name, app.Scopes, app.RedirectUris
|
app.Id, app.ClientId, app.ClientSecret, app.Name, app.Scopes, app.RedirectUris
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ public class AppsController(ILogger logger, IdentityContext db) : ControllerBase
|
||||||
{
|
{
|
||||||
var app = HttpContext.GetApplicationOrThrow();
|
var app = HttpContext.GetApplicationOrThrow();
|
||||||
|
|
||||||
return Ok(new Apps.GetSelfResponse(
|
return Ok(new AppsApi.GetSelfResponse(
|
||||||
app.Id,
|
app.Id,
|
||||||
app.ClientId,
|
app.ClientId,
|
||||||
withSecret ? app.ClientSecret : null,
|
withSecret ? app.ClientSecret : null,
|
||||||
|
|
|
@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore;
|
||||||
namespace Foxchat.Identity.Controllers.Oauth;
|
namespace Foxchat.Identity.Controllers.Oauth;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authenticate]
|
[ClientAuthenticate]
|
||||||
[Route("/_fox/ident/oauth/password")]
|
[Route("/_fox/ident/oauth/password")]
|
||||||
public class PasswordAuthController(ILogger logger, IdentityContext db, IClock clock) : ControllerBase
|
public class PasswordAuthController(ILogger logger, IdentityContext db, IClock clock) : ControllerBase
|
||||||
{
|
{
|
||||||
|
|
21
Foxchat.Identity/Controllers/UsersController.cs
Normal file
21
Foxchat.Identity/Controllers/UsersController.cs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
using Foxchat.Core;
|
||||||
|
using Foxchat.Core.Models;
|
||||||
|
using Foxchat.Identity.Database;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Foxchat.Identity.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("/_fox/ident/users")]
|
||||||
|
public class UsersController(ILogger logger, InstanceConfig config, IdentityContext db) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<IActionResult> GetUser(Ulid id)
|
||||||
|
{
|
||||||
|
var user = await db.Accounts.FirstOrDefaultAsync(a => a.Id == id);
|
||||||
|
if (user == null) throw new ApiError.NotFound("User not found.");
|
||||||
|
|
||||||
|
return Ok(new Users.User(user.Id.ToString(), user.Username, config.Domain, null));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Foxchat.Core.Middleware;
|
||||||
using Foxchat.Identity.Middleware;
|
using Foxchat.Identity.Middleware;
|
||||||
|
|
||||||
namespace Foxchat.Identity.Extensions;
|
namespace Foxchat.Identity.Extensions;
|
||||||
|
@ -8,15 +9,15 @@ public static class WebApplicationExtensions
|
||||||
{
|
{
|
||||||
return services
|
return services
|
||||||
.AddScoped<ErrorHandlerMiddleware>()
|
.AddScoped<ErrorHandlerMiddleware>()
|
||||||
.AddScoped<AuthenticationMiddleware>()
|
.AddScoped<ClientAuthenticationMiddleware>()
|
||||||
.AddScoped<AuthorizationMiddleware>();
|
.AddScoped<ClientAuthorizationMiddleware>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app)
|
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
return app
|
return app
|
||||||
.UseMiddleware<ErrorHandlerMiddleware>()
|
.UseMiddleware<ErrorHandlerMiddleware>()
|
||||||
.UseMiddleware<AuthenticationMiddleware>()
|
.UseMiddleware<ClientAuthenticationMiddleware>()
|
||||||
.UseMiddleware<AuthorizationMiddleware>();
|
.UseMiddleware<ClientAuthorizationMiddleware>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ using NodaTime;
|
||||||
|
|
||||||
namespace Foxchat.Identity.Middleware;
|
namespace Foxchat.Identity.Middleware;
|
||||||
|
|
||||||
public class AuthenticationMiddleware(
|
public class ClientAuthenticationMiddleware(
|
||||||
IdentityContext db,
|
IdentityContext db,
|
||||||
IClock clock
|
IClock clock
|
||||||
) : IMiddleware
|
) : IMiddleware
|
||||||
|
@ -16,7 +16,7 @@ public class AuthenticationMiddleware(
|
||||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
|
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
|
||||||
{
|
{
|
||||||
var endpoint = ctx.GetEndpoint();
|
var endpoint = ctx.GetEndpoint();
|
||||||
var metadata = endpoint?.Metadata.GetMetadata<AuthenticateAttribute>();
|
var metadata = endpoint?.Metadata.GetMetadata<ClientAuthenticateAttribute>();
|
||||||
|
|
||||||
if (metadata == null)
|
if (metadata == null)
|
||||||
{
|
{
|
||||||
|
@ -81,4 +81,4 @@ public static class HttpContextExtensions
|
||||||
}
|
}
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||||
public class AuthenticateAttribute : Attribute;
|
public class ClientAuthenticateAttribute : Attribute;
|
|
@ -4,7 +4,7 @@ using NodaTime;
|
||||||
|
|
||||||
namespace Foxchat.Identity.Middleware;
|
namespace Foxchat.Identity.Middleware;
|
||||||
|
|
||||||
public class AuthorizationMiddleware(
|
public class ClientAuthorizationMiddleware(
|
||||||
IdentityContext db,
|
IdentityContext db,
|
||||||
IClock clock
|
IClock clock
|
||||||
) : IMiddleware
|
) : IMiddleware
|
Loading…
Reference in a new issue