diff --git a/Foxchat.Chat/Controllers/Api/GuildsController.cs b/Foxchat.Chat/Controllers/Api/GuildsController.cs index bd8d0bb..ab99a6d 100644 --- a/Foxchat.Chat/Controllers/Api/GuildsController.cs +++ b/Foxchat.Chat/Controllers/Api/GuildsController.cs @@ -5,7 +5,6 @@ using Foxchat.Chat.Services; using Foxchat.Core.Models; using Foxchat.Core.Models.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using ApiError = Foxchat.Core.ApiError; namespace Foxchat.Chat.Controllers.Api; @@ -17,9 +16,10 @@ public class GuildsController(ILogger logger, ChatContext db, UserResolverServic [HttpPost] public async Task CreateGuild([FromBody] GuildsApi.CreateGuildRequest req) { - var (instance, _, userId) = HttpContext.GetSignatureWithUser(); + 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, userId); + var user = await userResolverService.ResolveUserAsync(instance, sig.UserId); var guild = new Guild { @@ -38,47 +38,10 @@ public class GuildsController(ILogger logger, ChatContext db, UserResolverServic await db.SaveChangesAsync(); return Ok(new Guilds.Guild( - guild.Id.ToString(), - guild.Name, + guild.Id.ToString(), + guild.Name, [user.Id.ToString()], [new Channels.PartialChannel(defaultChannel.Id.ToString(), defaultChannel.Name)]) ); } - - [HttpGet("{id}")] - public async Task GetGuild(Ulid id) - { - var (instance, _, userId) = HttpContext.GetSignatureWithUser(); - var guild = await db.Guilds - .Include(g => g.Channels) - .FirstOrDefaultAsync(g => - g.Id == id && g.Users.Any(u => u.RemoteUserId == userId && u.InstanceId == instance.Id)); - if (guild == null) throw new ApiError.NotFound("Guild not found"); - - return Ok(new Guilds.Guild( - guild.Id.ToString(), - guild.Name, - [guild.OwnerId.ToString()], - guild.Channels.Select(c => new Channels.PartialChannel(c.Id.ToString(), c.Name)) - )); - } - - [HttpGet("@me")] - public async Task GetUserGuilds() - { - var (instance, _, userId) = HttpContext.GetSignatureWithUser(); - var guilds = await db.Guilds - .Include(g => g.Channels) - .Where(g => g.Users.Any(u => u.RemoteUserId == userId && u.InstanceId == instance.Id)) - .ToListAsync(); - - var guildResponses = guilds.Select(g => new Guilds.Guild( - g.Id.ToString(), - g.Name, - [g.OwnerId.ToString()], - g.Channels.Select(c => new Channels.PartialChannel(c.Id.ToString(), c.Name)) - )); - - return Ok(guildResponses); - } } \ No newline at end of file diff --git a/Foxchat.Chat/Database/ChatContext.cs b/Foxchat.Chat/Database/ChatContext.cs index f8ac2a5..efbe8a1 100644 --- a/Foxchat.Chat/Database/ChatContext.cs +++ b/Foxchat.Chat/Database/ChatContext.cs @@ -1,7 +1,6 @@ using Foxchat.Chat.Database.Models; using Foxchat.Core; using Foxchat.Core.Database; -using Foxchat.Core.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Npgsql; @@ -11,7 +10,6 @@ namespace Foxchat.Chat.Database; public class ChatContext : IDatabaseContext { private readonly NpgsqlDataSource _dataSource; - private readonly ILoggerFactory? _loggerFactory; public override DbSet Instance { get; set; } public DbSet IdentityInstances { get; set; } @@ -20,7 +18,7 @@ public class ChatContext : IDatabaseContext public DbSet Channels { get; set; } public DbSet Messages { get; set; } - public ChatContext(InstanceConfig config, ILoggerFactory? loggerFactory) + public ChatContext(InstanceConfig config) { var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) { @@ -31,14 +29,12 @@ public class ChatContext : IDatabaseContext var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); dataSourceBuilder.UseNodaTime(); _dataSource = dataSourceBuilder.Build(); - _loggerFactory = loggerFactory; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseNpgsql(_dataSource, o => o.UseNodaTime()) - .UseSnakeCaseNamingConvention() - .UseLoggerFactory(_loggerFactory); + .UseSnakeCaseNamingConvention(); protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { @@ -77,6 +73,6 @@ public class DesignTimeIdentityContextFactory : IDesignTimeDbContextFactory() ?? new(); - return new ChatContext(config, null); + return new ChatContext(config); } } \ No newline at end of file diff --git a/Foxchat.Chat/Middleware/ServerAuthenticationMiddleware.cs b/Foxchat.Chat/Middleware/ServerAuthenticationMiddleware.cs index 4db585a..daea8dc 100644 --- a/Foxchat.Chat/Middleware/ServerAuthenticationMiddleware.cs +++ b/Foxchat.Chat/Middleware/ServerAuthenticationMiddleware.cs @@ -76,11 +76,4 @@ public static class HttpContextExtensions return ((IdentityInstance, SignatureData))obj!; } - - public static (IdentityInstance, SignatureData, string) GetSignatureWithUser(this HttpContext ctx) - { - var (instance, sig) = ctx.GetSignatureOrThrow(); - if (sig.UserId == null) throw new ApiError.IncomingFederationError("This endpoint requires a user ID."); - return (instance, sig, sig.UserId); - } } \ No newline at end of file diff --git a/Foxchat.Chat/Program.cs b/Foxchat.Chat/Program.cs index fa2bbb6..263167d 100644 --- a/Foxchat.Chat/Program.cs +++ b/Foxchat.Chat/Program.cs @@ -4,14 +4,13 @@ using Foxchat.Core; using Foxchat.Chat; using Foxchat.Chat.Database; using Foxchat.Chat.Extensions; -using Foxchat.Core.Extensions; using Newtonsoft.Json; var builder = WebApplication.CreateBuilder(args); var config = builder.AddConfiguration("chat.ini"); -builder.AddSerilog(); +builder.AddSerilog(config.LogEventLevel); await BuildInfo.ReadBuildInfo(); Log.Information("Starting Foxchat.Chat {Version} ({Hash})", BuildInfo.Version, BuildInfo.Hash); diff --git a/Foxchat.Chat/chat.ini b/Foxchat.Chat/chat.ini index 906acdb..21a32a0 100644 --- a/Foxchat.Chat/chat.ini +++ b/Foxchat.Chat/chat.ini @@ -2,6 +2,9 @@ Host = localhost Port = 7610 Domain = chat.fox.localhost +; The level to log things at. Valid settings: Verbose, Debug, Information, Warning, Error, Fatal +LogEventLevel = Debug + [Database] ; The database URL in ADO.NET format. Url = "Host=localhost;Database=foxchat_cs_chat;Username=foxchat;Password=password" @@ -10,11 +13,3 @@ Url = "Host=localhost;Database=foxchat_cs_chat;Username=foxchat;Password=passwor Timeout = 5 ; The maximum number of open connections. Defaults to 50. MaxPoolSize = 500 - -[Logging] -; The level to log things at. Valid settings: Verbose, Debug, Information, Warning, Error, Fatal -LogEventLevel = Debug -; Whether to log SQL queries. -LogQueries = true -; Optional logging to Seq -SeqLogUrl = http://localhost:5341 \ No newline at end of file diff --git a/Foxchat.Core/CoreConfig.cs b/Foxchat.Core/CoreConfig.cs index 236af8a..40a97b4 100644 --- a/Foxchat.Core/CoreConfig.cs +++ b/Foxchat.Core/CoreConfig.cs @@ -11,7 +11,9 @@ public class CoreConfig public string Address => $"{(Secure ? "https" : "http")}://{Host}:{Port}"; - public LoggingConfig Logging { get; set; } = new(); + public LogEventLevel LogEventLevel { get; set; } = LogEventLevel.Debug; + public string? SeqLogUrl { get; set; } + public DatabaseConfig Database { get; set; } = new(); public class DatabaseConfig @@ -20,11 +22,4 @@ public class CoreConfig public int? Timeout { get; set; } public int? MaxPoolSize { get; set; } } - - public class LoggingConfig - { - public LogEventLevel LogEventLevel { get; set; } = LogEventLevel.Debug; - public string? SeqLogUrl { get; set; } - public bool LogQueries { get; set; } = false; - } -} \ No newline at end of file +} diff --git a/Foxchat.Core/Extensions/ServiceCollectionExtensions.cs b/Foxchat.Core/Extensions/ServiceCollectionExtensions.cs index a41dd0b..f5d4893 100644 --- a/Foxchat.Core/Extensions/ServiceCollectionExtensions.cs +++ b/Foxchat.Core/Extensions/ServiceCollectionExtensions.cs @@ -7,32 +7,30 @@ using NodaTime; using Serilog; using Serilog.Events; -namespace Foxchat.Core.Extensions; +namespace Foxchat.Core; public static class ServiceCollectionExtensions { /// /// Adds Serilog to this service collection. This method also initializes Serilog so it should be called as early as possible, before any log calls. /// - public static void AddSerilog(this WebApplicationBuilder builder) + public static void AddSerilog(this WebApplicationBuilder builder, LogEventLevel level) { var config = builder.Configuration.Get() ?? new(); var logCfg = new LoggerConfiguration() .Enrich.FromLogContext() - .MinimumLevel.Is(config.Logging.LogEventLevel) + .MinimumLevel.Is(level) // 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. + // 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.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) .WriteTo.Console(); - if (config.Logging.SeqLogUrl != null) - logCfg.WriteTo.Seq(config.Logging.SeqLogUrl, restrictedToMinimumLevel: LogEventLevel.Verbose); + if (config.SeqLogUrl != null) + logCfg.WriteTo.Seq(config.SeqLogUrl, restrictedToMinimumLevel: LogEventLevel.Verbose); Log.Logger = logCfg.CreateLogger(); @@ -56,9 +54,9 @@ public static class ServiceCollectionExtensions return services; } - public static T AddConfiguration(this WebApplicationBuilder builder, string? configFile = null) - where T : class, new() + public static T AddConfiguration(this WebApplicationBuilder builder, string? configFile = null) where T : class, new() { + builder.Configuration.Sources.Clear(); builder.Configuration.AddConfiguration(configFile); @@ -78,4 +76,4 @@ public static class ServiceCollectionExtensions .AddIniFile(file, optional: false, reloadOnChange: true) .AddEnvironmentVariables(); } -} \ No newline at end of file +} diff --git a/Foxchat.Identity/Controllers/Oauth/PasswordAuthController.cs b/Foxchat.Identity/Controllers/Oauth/PasswordAuthController.cs index 06a6a6e..a4f5080 100644 --- a/Foxchat.Identity/Controllers/Oauth/PasswordAuthController.cs +++ b/Foxchat.Identity/Controllers/Oauth/PasswordAuthController.cs @@ -25,7 +25,7 @@ public class PasswordAuthController(ILogger logger, IdentityContext db, IClock c var appToken = HttpContext.GetToken() ?? throw new UnreachableException(); // GetApplicationOrThrow already gets the token and throws if it's null - var appScopes = appToken.Scopes.ExpandScopes(); + var appScopes = appToken.ExpandScopes(); if (req.Scopes.Except(appScopes).Any()) throw new ApiError.Forbidden("Cannot request token scopes that are not allowed for this token", @@ -54,7 +54,7 @@ public class PasswordAuthController(ILogger logger, IdentityContext db, IClock c { var app = HttpContext.GetApplicationOrThrow(); var appToken = HttpContext.GetToken() ?? throw new UnreachableException(); - var appScopes = appToken.Scopes.ExpandScopes(); + var appScopes = appToken.ExpandScopes(); if (req.Scopes.Except(appScopes).Any()) throw new ApiError.Forbidden("Cannot request token scopes that are not allowed for this token", diff --git a/Foxchat.Identity/Controllers/Oauth/TokenController.cs b/Foxchat.Identity/Controllers/Oauth/TokenController.cs index 20c5924..ed7dfc8 100644 --- a/Foxchat.Identity/Controllers/Oauth/TokenController.cs +++ b/Foxchat.Identity/Controllers/Oauth/TokenController.cs @@ -15,7 +15,7 @@ public class TokenController(ILogger logger, IdentityContext db, IClock clock) : public async Task PostToken([FromBody] PostTokenRequest req) { var app = await db.GetApplicationAsync(req.ClientId, req.ClientSecret); - var appScopes = app.Scopes.ExpandScopes(); + var appScopes = app.ExpandScopes(); var scopes = req.Scope.Split(' '); if (scopes.Except(appScopes).Any()) @@ -25,9 +25,9 @@ public class TokenController(ILogger logger, IdentityContext db, IClock clock) : switch (req.GrantType) { - case OauthUtils.ClientCredentials: + case "client_credentials": return await HandleClientCredentialsAsync(app, scopes); - case OauthUtils.AuthorizationCode: + case "authorization_code": // TODO break; default: diff --git a/Foxchat.Identity/Controllers/UsersController.cs b/Foxchat.Identity/Controllers/UsersController.cs index 6302183..9e9c32d 100644 --- a/Foxchat.Identity/Controllers/UsersController.cs +++ b/Foxchat.Identity/Controllers/UsersController.cs @@ -1,17 +1,12 @@ using Foxchat.Core; using Foxchat.Core.Models; using Foxchat.Identity.Database; -using Foxchat.Identity.Database.Models; -using Foxchat.Identity.Middleware; -using Foxchat.Identity.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; namespace Foxchat.Identity.Controllers; [ApiController] -[ClientAuthenticate] [Route("/_fox/ident/users")] public class UsersController(ILogger logger, InstanceConfig config, IdentityContext db) : ControllerBase { @@ -23,30 +18,4 @@ public class UsersController(ILogger logger, InstanceConfig config, IdentityCont return Ok(new Users.User(user.Id.ToString(), user.Username, config.Domain, null)); } - - [HttpGet("@me")] - [Authorize("identify")] - public IActionResult GetMe() - { - var acct = HttpContext.GetAccountOrThrow(); - var token = HttpContext.GetToken()!; - var showEmail = token.Scopes.ExpandScopes().Contains("email"); - - return Ok(new MeUser( - acct.Id, - acct.Username, - acct.Role, - null, - showEmail ? acct.Email : null - )); - } - - public record MeUser( - Ulid Id, - string Username, - Account.AccountRole Role, - string? AvatarUrl, - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - string? Email - ); } \ No newline at end of file diff --git a/Foxchat.Identity/Database/IdentityContext.cs b/Foxchat.Identity/Database/IdentityContext.cs index efc4850..e983fac 100644 --- a/Foxchat.Identity/Database/IdentityContext.cs +++ b/Foxchat.Identity/Database/IdentityContext.cs @@ -1,6 +1,5 @@ using Foxchat.Core; using Foxchat.Core.Database; -using Foxchat.Core.Extensions; using Foxchat.Identity.Database.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; @@ -11,7 +10,6 @@ namespace Foxchat.Identity.Database; public class IdentityContext : IDatabaseContext { private readonly NpgsqlDataSource _dataSource; - private readonly ILoggerFactory? _loggerFactory; public override DbSet Instance { get; set; } public DbSet Accounts { get; set; } @@ -20,7 +18,7 @@ public class IdentityContext : IDatabaseContext public DbSet Tokens { get; set; } public DbSet GuildAccounts { get; set; } - public IdentityContext(InstanceConfig config, ILoggerFactory? loggerFactory) + public IdentityContext(InstanceConfig config) { var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) { @@ -31,14 +29,12 @@ public class IdentityContext : IDatabaseContext var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); dataSourceBuilder.UseNodaTime(); _dataSource = dataSourceBuilder.Build(); - _loggerFactory = loggerFactory; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder - .UseNpgsql(_dataSource, o => o.UseNodaTime()) - .UseSnakeCaseNamingConvention() - .UseLoggerFactory(_loggerFactory); + => optionsBuilder + .UseNpgsql(_dataSource, o => o.UseNodaTime()) + .UseSnakeCaseNamingConvention(); protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { @@ -71,6 +67,6 @@ public class DesignTimeIdentityContextFactory : IDesignTimeDbContextFactory() ?? new(); - return new IdentityContext(config, null); + return new IdentityContext(config); } } diff --git a/Foxchat.Identity/Middleware/ClientAuthorizationMiddleware.cs b/Foxchat.Identity/Middleware/ClientAuthorizationMiddleware.cs index 2e6499d..701fb05 100644 --- a/Foxchat.Identity/Middleware/ClientAuthorizationMiddleware.cs +++ b/Foxchat.Identity/Middleware/ClientAuthorizationMiddleware.cs @@ -24,8 +24,8 @@ public class ClientAuthorizationMiddleware( var token = ctx.GetToken(); if (token == null || token.Expires < clock.GetCurrentInstant()) throw new ApiError.Unauthorized("This endpoint requires an authenticated user."); - if (attribute.Scopes.Length > 0 && attribute.Scopes.Except(token.Scopes.ExpandScopes()).Any()) - throw new ApiError.Forbidden("This endpoint requires ungranted scopes.", attribute.Scopes.Except(token.Scopes.ExpandScopes())); + if (attribute.Scopes.Length > 0 && attribute.Scopes.Except(token.ExpandScopes()).Any()) + throw new ApiError.Forbidden("This endpoint requires ungranted scopes.", attribute.Scopes.Except(token.ExpandScopes())); await next(ctx); } diff --git a/Foxchat.Identity/Program.cs b/Foxchat.Identity/Program.cs index 035feb1..8e0c47e 100644 --- a/Foxchat.Identity/Program.cs +++ b/Foxchat.Identity/Program.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json.Serialization; using Serilog; using Foxchat.Core; -using Foxchat.Core.Extensions; using Foxchat.Identity; using Foxchat.Identity.Database; using Foxchat.Identity.Services; @@ -12,7 +11,7 @@ var builder = WebApplication.CreateBuilder(args); var config = builder.AddConfiguration("identity.ini"); -builder.AddSerilog(); +builder.AddSerilog(config.LogEventLevel); await BuildInfo.ReadBuildInfo(); Log.Information("Starting Foxchat.Identity {Version} ({Hash})", BuildInfo.Version, BuildInfo.Hash); diff --git a/Foxchat.Identity/Utils/OauthUtils.cs b/Foxchat.Identity/Utils/OauthUtils.cs index d6d5b2c..bf698a2 100644 --- a/Foxchat.Identity/Utils/OauthUtils.cs +++ b/Foxchat.Identity/Utils/OauthUtils.cs @@ -6,10 +6,7 @@ namespace Foxchat.Identity.Utils; public static class OauthUtils { - public const string ClientCredentials = "client_credentials"; - public const string AuthorizationCode = "authorization_code"; - - public static readonly string[] Scopes = ["identify", "email", "guilds", "chat_client"]; + public static readonly string[] Scopes = ["identify", "chat_client"]; private static readonly string[] ForbiddenSchemes = ["javascript", "file", "data", "mailto", "tel"]; private const string OobUri = "urn:ietf:wg:oauth:2.0:oob"; @@ -28,7 +25,11 @@ public static class OauthUtils } } - public static string[] ExpandScopes(this string[] scopes) => scopes.Contains("chat_client") + public static string[] ExpandScopes(this Token token) => token.Scopes.Contains("chat_client") ? Scopes - : scopes; + : token.Scopes; + + public static string[] ExpandScopes(this Application app) => app.Scopes.Contains("chat_client") + ? Scopes + : app.Scopes; } \ No newline at end of file diff --git a/Foxchat.Identity/identity.ini b/Foxchat.Identity/identity.ini index 7f4172c..d4b3c40 100644 --- a/Foxchat.Identity/identity.ini +++ b/Foxchat.Identity/identity.ini @@ -2,6 +2,11 @@ Host = localhost Port = 7611 Domain = id.fox.localhost +; The level to log things at. Valid settings: Verbose, Debug, Information, Warning, Error, Fatal +LogEventLevel = Debug +; Optional logging to Seq +SeqLogUrl = http://localhost:5341 + [Database] ; The database URL in ADO.NET format. Url = "Host=localhost;Database=foxchat_cs_ident;Username=foxchat;Password=password" @@ -10,11 +15,3 @@ Url = "Host=localhost;Database=foxchat_cs_ident;Username=foxchat;Password=passwo Timeout = 5 ; The maximum number of open connections. Defaults to 50. MaxPoolSize = 500 - -[Logging] -; The level to log things at. Valid settings: Verbose, Debug, Information, Warning, Error, Fatal -LogEventLevel = Debug -; Whether to log SQL queries. -LogQueries = true -; Optional logging to Seq -SeqLogUrl = http://localhost:5341