diff --git a/.editorconfig b/.editorconfig index 00c5659..9fec5aa 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,6 +2,3 @@ # CS9113: Parameter is unread. dotnet_diagnostic.CS9113.severity = silent - -# EntityFramework.ModelValidation.UnlimitedStringLength -resharper_entity_framework_model_validation_unlimited_string_length_highlighting=none \ No newline at end of file diff --git a/.gitignore b/.gitignore index d01c4a3..cd1b080 100644 --- a/.gitignore +++ b/.gitignore @@ -1,47 +1,3 @@ bin/ obj/ .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 diff --git a/Foxchat.Chat/Controllers/Api/GuildsController.cs b/Foxchat.Chat/Controllers/Api/GuildsController.cs deleted file mode 100644 index bd8d0bb..0000000 --- a/Foxchat.Chat/Controllers/Api/GuildsController.cs +++ /dev/null @@ -1,84 +0,0 @@ -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 Microsoft.EntityFrameworkCore; -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 CreateGuild([FromBody] GuildsApi.CreateGuildRequest req) - { - var (instance, _, userId) = HttpContext.GetSignatureWithUser(); - - var user = await userResolverService.ResolveUserAsync(instance, 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)]) - ); - } - - [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/Controllers/HelloController.cs b/Foxchat.Chat/Controllers/HelloController.cs index 0c0c639..f0e3ca4 100644 --- a/Foxchat.Chat/Controllers/HelloController.cs +++ b/Foxchat.Chat/Controllers/HelloController.cs @@ -10,7 +10,7 @@ using ApiError = Foxchat.Core.ApiError; namespace Foxchat.Chat.Controllers; [ApiController] -[ServerUnauthenticated] +[Unauthenticated] [Route("/_fox/chat/hello")] public class HelloController( ILogger logger, @@ -27,8 +27,6 @@ public class HelloController( if (!HttpContext.ExtractRequestData(out var signature, out var domain, out var signatureData)) 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)) throw new ApiError.IncomingFederationError("Signature is not valid."); 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/Database/Models/User.cs b/Foxchat.Chat/Database/Models/User.cs index 26f2d5d..18a7054 100644 --- a/Foxchat.Chat/Database/Models/User.cs +++ b/Foxchat.Chat/Database/Models/User.cs @@ -1,6 +1,3 @@ -using Foxchat.Core.Models; -using NodaTime; - namespace Foxchat.Chat.Database.Models; public class User : BaseModel @@ -11,7 +8,6 @@ public class User : BaseModel public string Username { get; init; } = null!; public string? Avatar { get; set; } - public Instant LastFetchedAt { get; set; } public List Guilds { get; } = []; public List OwnedGuilds { get; } = []; diff --git a/Foxchat.Chat/Extensions/WebApplicationExtensions.cs b/Foxchat.Chat/Extensions/WebApplicationExtensions.cs index e48d404..4900ba9 100644 --- a/Foxchat.Chat/Extensions/WebApplicationExtensions.cs +++ b/Foxchat.Chat/Extensions/WebApplicationExtensions.cs @@ -1,6 +1,4 @@ using Foxchat.Chat.Middleware; -using Foxchat.Chat.Services; -using Foxchat.Core.Middleware; namespace Foxchat.Chat.Extensions; @@ -9,20 +7,12 @@ public static class WebApplicationExtensions public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) { return services - .AddScoped() - .AddScoped(); + .AddScoped(); } public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app) { return app - .UseMiddleware() - .UseMiddleware(); - } - - public static IServiceCollection AddChatServices(this IServiceCollection services) - { - return services - .AddScoped(); + .UseMiddleware(); } } \ No newline at end of file diff --git a/Foxchat.Chat/Middleware/ServerAuthenticationMiddleware.cs b/Foxchat.Chat/Middleware/AuthenticationMiddleware.cs similarity index 75% rename from Foxchat.Chat/Middleware/ServerAuthenticationMiddleware.cs rename to Foxchat.Chat/Middleware/AuthenticationMiddleware.cs index 4db585a..cd522d8 100644 --- a/Foxchat.Chat/Middleware/ServerAuthenticationMiddleware.cs +++ b/Foxchat.Chat/Middleware/AuthenticationMiddleware.cs @@ -7,14 +7,14 @@ using Microsoft.EntityFrameworkCore; namespace Foxchat.Chat.Middleware; -public class ServerAuthenticationMiddleware(ILogger logger, ChatContext db, RequestSigningService requestSigningService) +public class AuthenticationMiddleware(ILogger logger, ChatContext db, RequestSigningService requestSigningService) : IMiddleware { public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { var endpoint = ctx.GetEndpoint(); // Endpoints require server authentication by default, unless they have the [Unauthenticated] attribute. - var metadata = endpoint?.Metadata.GetMetadata(); + var metadata = endpoint?.Metadata.GetMetadata(); if (metadata != null) { await next(ctx); @@ -41,11 +41,8 @@ public class ServerAuthenticationMiddleware(ILogger logger, ChatContext db, Requ } } -/// -/// Attribute to be put on controllers or methods to indicate that it does not require a signed request. -/// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class ServerUnauthenticatedAttribute : Attribute; +public class UnauthenticatedAttribute : Attribute; public static class HttpContextExtensions { @@ -76,11 +73,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/Migrations/20240521191115_AddLastFetchedAtToUsers.Designer.cs b/Foxchat.Chat/Migrations/20240521191115_AddLastFetchedAtToUsers.Designer.cs deleted file mode 100644 index 745ab75..0000000 --- a/Foxchat.Chat/Migrations/20240521191115_AddLastFetchedAtToUsers.Designer.cs +++ /dev/null @@ -1,329 +0,0 @@ -// -using System; -using Foxchat.Chat.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Foxchat.Chat.Migrations -{ - [DbContext(typeof(ChatContext))] - [Migration("20240521191115_AddLastFetchedAtToUsers")] - partial class AddLastFetchedAtToUsers - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Foxchat.Chat.Database.Models.Channel", b => - { - b.Property("Id") - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("GuildId") - .HasColumnType("uuid") - .HasColumnName("guild_id"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("Topic") - .HasColumnType("text") - .HasColumnName("topic"); - - b.HasKey("Id") - .HasName("pk_channels"); - - b.HasIndex("GuildId") - .HasDatabaseName("ix_channels_guild_id"); - - b.ToTable("channels", (string)null); - }); - - modelBuilder.Entity("Foxchat.Chat.Database.Models.Guild", b => - { - b.Property("Id") - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("OwnerId") - .HasColumnType("uuid") - .HasColumnName("owner_id"); - - b.HasKey("Id") - .HasName("pk_guilds"); - - b.HasIndex("OwnerId") - .HasDatabaseName("ix_guilds_owner_id"); - - b.ToTable("guilds", (string)null); - }); - - modelBuilder.Entity("Foxchat.Chat.Database.Models.IdentityInstance", b => - { - b.Property("Id") - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("BaseUrl") - .IsRequired() - .HasColumnType("text") - .HasColumnName("base_url"); - - b.Property("Domain") - .IsRequired() - .HasColumnType("text") - .HasColumnName("domain"); - - b.Property("PublicKey") - .IsRequired() - .HasColumnType("text") - .HasColumnName("public_key"); - - b.Property("Reason") - .HasColumnType("text") - .HasColumnName("reason"); - - b.Property("Status") - .HasColumnType("integer") - .HasColumnName("status"); - - b.HasKey("Id") - .HasName("pk_identity_instances"); - - b.HasIndex("Domain") - .IsUnique() - .HasDatabaseName("ix_identity_instances_domain"); - - b.ToTable("identity_instances", (string)null); - }); - - modelBuilder.Entity("Foxchat.Chat.Database.Models.Message", b => - { - b.Property("Id") - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("AuthorId") - .HasColumnType("uuid") - .HasColumnName("author_id"); - - b.Property("ChannelId") - .HasColumnType("uuid") - .HasColumnName("channel_id"); - - b.Property("Content") - .HasColumnType("text") - .HasColumnName("content"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("updated_at"); - - b.HasKey("Id") - .HasName("pk_messages"); - - b.HasIndex("AuthorId") - .HasDatabaseName("ix_messages_author_id"); - - b.HasIndex("ChannelId") - .HasDatabaseName("ix_messages_channel_id"); - - b.ToTable("messages", (string)null); - }); - - modelBuilder.Entity("Foxchat.Chat.Database.Models.User", b => - { - b.Property("Id") - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("Avatar") - .HasColumnType("text") - .HasColumnName("avatar"); - - b.Property("InstanceId") - .HasColumnType("uuid") - .HasColumnName("instance_id"); - - b.Property("LastFetchedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_fetched_at"); - - b.Property("RemoteUserId") - .IsRequired() - .HasColumnType("text") - .HasColumnName("remote_user_id"); - - b.Property("Username") - .IsRequired() - .HasColumnType("text") - .HasColumnName("username"); - - b.HasKey("Id") - .HasName("pk_users"); - - b.HasIndex("InstanceId") - .HasDatabaseName("ix_users_instance_id"); - - b.HasIndex("RemoteUserId", "InstanceId") - .IsUnique() - .HasDatabaseName("ix_users_remote_user_id_instance_id"); - - b.HasIndex("Username", "InstanceId") - .IsUnique() - .HasDatabaseName("ix_users_username_instance_id"); - - b.ToTable("users", (string)null); - }); - - modelBuilder.Entity("Foxchat.Core.Database.Instance", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("PrivateKey") - .IsRequired() - .HasColumnType("text") - .HasColumnName("private_key"); - - b.Property("PublicKey") - .IsRequired() - .HasColumnType("text") - .HasColumnName("public_key"); - - b.HasKey("Id") - .HasName("pk_instance"); - - b.ToTable("instance", (string)null); - }); - - modelBuilder.Entity("GuildUser", b => - { - b.Property("GuildsId") - .HasColumnType("uuid") - .HasColumnName("guilds_id"); - - b.Property("UsersId") - .HasColumnType("uuid") - .HasColumnName("users_id"); - - b.HasKey("GuildsId", "UsersId") - .HasName("pk_guild_user"); - - b.HasIndex("UsersId") - .HasDatabaseName("ix_guild_user_users_id"); - - b.ToTable("guild_user", (string)null); - }); - - modelBuilder.Entity("Foxchat.Chat.Database.Models.Channel", b => - { - b.HasOne("Foxchat.Chat.Database.Models.Guild", "Guild") - .WithMany("Channels") - .HasForeignKey("GuildId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_channels_guilds_guild_id"); - - b.Navigation("Guild"); - }); - - modelBuilder.Entity("Foxchat.Chat.Database.Models.Guild", b => - { - b.HasOne("Foxchat.Chat.Database.Models.User", "Owner") - .WithMany("OwnedGuilds") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_guilds_users_owner_id"); - - b.Navigation("Owner"); - }); - - modelBuilder.Entity("Foxchat.Chat.Database.Models.Message", b => - { - b.HasOne("Foxchat.Chat.Database.Models.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_messages_users_author_id"); - - b.HasOne("Foxchat.Chat.Database.Models.Channel", "Channel") - .WithMany() - .HasForeignKey("ChannelId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_messages_channels_channel_id"); - - b.Navigation("Author"); - - b.Navigation("Channel"); - }); - - modelBuilder.Entity("Foxchat.Chat.Database.Models.User", b => - { - b.HasOne("Foxchat.Chat.Database.Models.IdentityInstance", "Instance") - .WithMany() - .HasForeignKey("InstanceId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_users_identity_instances_instance_id"); - - b.Navigation("Instance"); - }); - - modelBuilder.Entity("GuildUser", b => - { - b.HasOne("Foxchat.Chat.Database.Models.Guild", null) - .WithMany() - .HasForeignKey("GuildsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_guild_user_guilds_guilds_id"); - - b.HasOne("Foxchat.Chat.Database.Models.User", null) - .WithMany() - .HasForeignKey("UsersId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_guild_user_users_users_id"); - }); - - modelBuilder.Entity("Foxchat.Chat.Database.Models.Guild", b => - { - b.Navigation("Channels"); - }); - - modelBuilder.Entity("Foxchat.Chat.Database.Models.User", b => - { - b.Navigation("OwnedGuilds"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Foxchat.Chat/Migrations/20240521191115_AddLastFetchedAtToUsers.cs b/Foxchat.Chat/Migrations/20240521191115_AddLastFetchedAtToUsers.cs deleted file mode 100644 index f4a99ba..0000000 --- a/Foxchat.Chat/Migrations/20240521191115_AddLastFetchedAtToUsers.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; - -#nullable disable - -namespace Foxchat.Chat.Migrations -{ - /// - public partial class AddLastFetchedAtToUsers : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "last_fetched_at", - table: "users", - type: "timestamp with time zone", - nullable: false, - defaultValue: NodaTime.Instant.FromUnixTimeTicks(0L)); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "last_fetched_at", - table: "users"); - } - } -} diff --git a/Foxchat.Chat/Migrations/ChatContextModelSnapshot.cs b/Foxchat.Chat/Migrations/ChatContextModelSnapshot.cs index 184e756..f560113 100644 --- a/Foxchat.Chat/Migrations/ChatContextModelSnapshot.cs +++ b/Foxchat.Chat/Migrations/ChatContextModelSnapshot.cs @@ -162,10 +162,6 @@ namespace Foxchat.Chat.Migrations .HasColumnType("uuid") .HasColumnName("instance_id"); - b.Property("LastFetchedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_fetched_at"); - b.Property("RemoteUserId") .IsRequired() .HasColumnType("text") diff --git a/Foxchat.Chat/Program.cs b/Foxchat.Chat/Program.cs index fa2bbb6..6d94171 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); @@ -35,7 +34,6 @@ builder.Services builder.Services .AddCoreServices() - .AddChatServices() .AddCustomMiddleware() .AddEndpointsApiExplorer() .AddSwaggerGen(); diff --git a/Foxchat.Chat/Services/UserResolverService.cs b/Foxchat.Chat/Services/UserResolverService.cs deleted file mode 100644 index 87f56eb..0000000 --- a/Foxchat.Chat/Services/UserResolverService.cs +++ /dev/null @@ -1,35 +0,0 @@ -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 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(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; - } -} \ No newline at end of file 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/HttpContextExtensions.cs b/Foxchat.Core/Extensions/HttpContextExtensions.cs index f00c8e4..9b5db59 100644 --- a/Foxchat.Core/Extensions/HttpContextExtensions.cs +++ b/Foxchat.Core/Extensions/HttpContextExtensions.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Foxchat.Core.Federation; using Microsoft.AspNetCore.Http; @@ -6,12 +5,11 @@ namespace Foxchat.Core.Extensions; public static class HttpContextExtensions { - public static bool ExtractRequestData(this HttpContext ctx, [NotNullWhen(true)] out string? signature, - [NotNullWhen(true)] out string? domain, [NotNullWhen(true)] out SignatureData? data) + public static bool ExtractRequestData(this HttpContext ctx, out string signature, out string domain, out SignatureData data) { - signature = null; - domain = null; - data = null; + signature = string.Empty; + domain = string.Empty; + data = SignatureData.Empty; if (!ctx.Request.Headers.TryGetValue(RequestSigningService.SIGNATURE_HEADER, out var encodedSignature)) return false; 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.Core/Federation/RequestSigningService.cs b/Foxchat.Core/Federation/RequestSigningService.cs index 2185277..54ec6bf 100644 --- a/Foxchat.Core/Federation/RequestSigningService.cs +++ b/Foxchat.Core/Federation/RequestSigningService.cs @@ -33,18 +33,19 @@ public partial class RequestSigningService(ILogger logger, IClock clock, IDataba public bool VerifySignature( string publicKey, string encodedSignature, SignatureData data) { - if (data.Host != _config.Domain) - throw new ApiError.IncomingFederationError("Request is not for this instance"); - - var now = _clock.GetCurrentInstant(); - if (now + Duration.FromMinutes(1) < data.Time) - throw new ApiError.IncomingFederationError("Request was made in the future"); - if (now - Duration.FromMinutes(1) > data.Time) - throw new ApiError.IncomingFederationError("Request was made too long ago"); - var rsa = RSA.Create(); rsa.ImportFromPem(publicKey); + var now = _clock.GetCurrentInstant(); + if ((now + Duration.FromMinutes(1)) < data.Time) + { + throw new ApiError.IncomingFederationError("Request was made in the future"); + } + else if ((now - Duration.FromMinutes(1)) > data.Time) + { + throw new ApiError.IncomingFederationError("Request was made too long ago"); + } + var plaintext = GeneratePlaintext(data); var plaintextBytes = Encoding.UTF8.GetBytes(plaintext); var hash = SHA256.HashData(plaintextBytes); @@ -69,9 +70,7 @@ public partial class RequestSigningService(ILogger logger, IClock clock, IDataba 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); public static Instant ParseTime(string header) => _pattern.Parse(header).GetValueOrThrow(); -} \ No newline at end of file +} diff --git a/Foxchat.Core/Models/Channels.cs b/Foxchat.Core/Models/Channels.cs deleted file mode 100644 index b84c114..0000000 --- a/Foxchat.Core/Models/Channels.cs +++ /dev/null @@ -1,8 +0,0 @@ -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); -} \ No newline at end of file diff --git a/Foxchat.Core/Models/Guilds.cs b/Foxchat.Core/Models/Guilds.cs deleted file mode 100644 index ac0a6a4..0000000 --- a/Foxchat.Core/Models/Guilds.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Newtonsoft.Json; - -namespace Foxchat.Core.Models; - -public static class Guilds -{ - public record Guild( - string Id, - string Name, - IEnumerable OwnerIds, - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - IEnumerable? Channels - ); -} \ No newline at end of file diff --git a/Foxchat.Core/Models/Http/AppsApi.cs b/Foxchat.Core/Models/Http/Apps.cs similarity index 93% rename from Foxchat.Core/Models/Http/AppsApi.cs rename to Foxchat.Core/Models/Http/Apps.cs index 909ce4d..8b670f4 100644 --- a/Foxchat.Core/Models/Http/AppsApi.cs +++ b/Foxchat.Core/Models/Http/Apps.cs @@ -1,6 +1,6 @@ namespace Foxchat.Core.Models.Http; -public static class AppsApi +public static class Apps { public record CreateRequest(string Name, string[] Scopes, string[] RedirectUris); public record CreateResponse(Ulid Id, string ClientId, string ClientSecret, string Name, string[] Scopes, string[] RedirectUris); diff --git a/Foxchat.Core/Models/Http/GuildsApi.cs b/Foxchat.Core/Models/Http/GuildsApi.cs deleted file mode 100644 index e9f7eaf..0000000 --- a/Foxchat.Core/Models/Http/GuildsApi.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Foxchat.Core.Models.Http; - -public static class GuildsApi -{ - public record CreateGuildRequest(string Name); -} \ No newline at end of file diff --git a/Foxchat.Core/Models/Users.cs b/Foxchat.Core/Models/Users.cs deleted file mode 100644 index 6914a44..0000000 --- a/Foxchat.Core/Models/Users.cs +++ /dev/null @@ -1,8 +0,0 @@ -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); -} \ No newline at end of file diff --git a/Foxchat.Identity/Controllers/Oauth/AppsController.cs b/Foxchat.Identity/Controllers/Oauth/AppsController.cs index 1db1b74..402b138 100644 --- a/Foxchat.Identity/Controllers/Oauth/AppsController.cs +++ b/Foxchat.Identity/Controllers/Oauth/AppsController.cs @@ -9,12 +9,12 @@ using Microsoft.AspNetCore.Mvc; namespace Foxchat.Identity.Controllers.Oauth; [ApiController] -[ClientAuthenticate] +[Authenticate] [Route("/_fox/ident/oauth/apps")] public class AppsController(ILogger logger, IdentityContext db) : ControllerBase { [HttpPost] - public async Task CreateApplication([FromBody] AppsApi.CreateRequest req) + public async Task CreateApplication([FromBody] Apps.CreateRequest req) { var app = Application.Create(req.Name, req.Scopes, req.RedirectUris); 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); - return Ok(new AppsApi.CreateResponse( + return Ok(new Apps.CreateResponse( 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(); - return Ok(new AppsApi.GetSelfResponse( + return Ok(new Apps.GetSelfResponse( app.Id, app.ClientId, withSecret ? app.ClientSecret : null, diff --git a/Foxchat.Identity/Controllers/Oauth/PasswordAuthController.cs b/Foxchat.Identity/Controllers/Oauth/PasswordAuthController.cs index 06a6a6e..2d2c729 100644 --- a/Foxchat.Identity/Controllers/Oauth/PasswordAuthController.cs +++ b/Foxchat.Identity/Controllers/Oauth/PasswordAuthController.cs @@ -5,14 +5,13 @@ using Microsoft.AspNetCore.Identity; using Foxchat.Identity.Database.Models; using Foxchat.Core; using System.Diagnostics; -using Foxchat.Identity.Utils; using NodaTime; using Microsoft.EntityFrameworkCore; namespace Foxchat.Identity.Controllers.Oauth; [ApiController] -[ClientAuthenticate] +[Authenticate] [Route("/_fox/ident/oauth/password")] public class PasswordAuthController(ILogger logger, IdentityContext db, IClock clock) : ControllerBase { @@ -25,11 +24,10 @@ 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(); - if (req.Scopes.Except(appScopes).Any()) + if (req.Scopes.Except(appToken.Scopes).Any()) throw new ApiError.Forbidden("Cannot request token scopes that are not allowed for this token", - req.Scopes.Except(appScopes)); + req.Scopes.Except(appToken.Scopes)); var acct = new Account { @@ -54,11 +52,10 @@ 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(); - if (req.Scopes.Except(appScopes).Any()) + if (req.Scopes.Except(appToken.Scopes).Any()) throw new ApiError.Forbidden("Cannot request token scopes that are not allowed for this token", - req.Scopes.Except(appScopes)); + req.Scopes.Except(appToken.Scopes)); var acct = await db.Accounts.FirstOrDefaultAsync(a => a.Email == req.Email) ?? throw new ApiError.NotFound("No user with that email found, or password is incorrect"); diff --git a/Foxchat.Identity/Controllers/Oauth/TokenController.cs b/Foxchat.Identity/Controllers/Oauth/TokenController.cs index 20c5924..b01ee77 100644 --- a/Foxchat.Identity/Controllers/Oauth/TokenController.cs +++ b/Foxchat.Identity/Controllers/Oauth/TokenController.cs @@ -1,7 +1,6 @@ using Foxchat.Core; using Foxchat.Identity.Database; using Foxchat.Identity.Database.Models; -using Foxchat.Identity.Utils; using Microsoft.AspNetCore.Mvc; using NodaTime; @@ -15,19 +14,18 @@ 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 scopes = req.Scope.Split(' '); - if (scopes.Except(appScopes).Any()) + if (scopes.Except(app.Scopes).Any()) { - throw new ApiError.Forbidden("Invalid or unauthorized scopes", scopes.Except(appScopes)); + throw new ApiError.BadRequest("Invalid or unauthorized scopes"); } 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/Proxy/GuildsProxyController.cs b/Foxchat.Identity/Controllers/Proxy/GuildsProxyController.cs deleted file mode 100644 index 9b5a8d4..0000000 --- a/Foxchat.Identity/Controllers/Proxy/GuildsProxyController.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Foxchat.Core.Federation; -using Foxchat.Core.Models; -using Foxchat.Core.Models.Http; -using Foxchat.Identity.Middleware; -using Foxchat.Identity.Services; -using Microsoft.AspNetCore.Mvc; - -namespace Foxchat.Identity.Controllers.Proxy; - -[Route("/_fox/proxy/guilds")] -public class GuildsProxyController( - ILogger logger, - ChatInstanceResolverService chatInstanceResolverService, - RequestSigningService requestSigningService) - : ProxyControllerBase(logger, chatInstanceResolverService, requestSigningService) -{ - [Authorize("chat_client")] - [HttpPost] - public Task CreateGuild([FromBody] GuildsApi.CreateGuildRequest req) => - Proxy(HttpMethod.Post, req); -} \ No newline at end of file diff --git a/Foxchat.Identity/Controllers/Proxy/ProxyControllerBase.cs b/Foxchat.Identity/Controllers/Proxy/ProxyControllerBase.cs deleted file mode 100644 index 4728ca4..0000000 --- a/Foxchat.Identity/Controllers/Proxy/ProxyControllerBase.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Foxchat.Core; -using Foxchat.Core.Federation; -using Foxchat.Identity.Middleware; -using Foxchat.Identity.Services; -using Microsoft.AspNetCore.Mvc; - -namespace Foxchat.Identity.Controllers.Proxy; - -[ApiController] -[ClientAuthenticate] -public class ProxyControllerBase( - ILogger logger, - ChatInstanceResolverService chatInstanceResolverService, - RequestSigningService requestSigningService) : ControllerBase -{ - internal async Task Proxy(HttpMethod method, object? body = null) where TResponse : class - { - var acct = HttpContext.GetAccountOrThrow(); - - var path = HttpContext.Request.Path.ToString(); - if (!path.StartsWith("/_fox/proxy")) - throw new FoxchatError("Proxy used for endpoint that does not start with /_fox/proxy"); - path = $"/_fox/chat/{path[12..]}"; - - if (!HttpContext.Request.Headers.TryGetValue(RequestSigningService.SERVER_HEADER, out var serverHeader)) - throw new ApiError.BadRequest($"Invalid or missing {RequestSigningService.SERVER_HEADER} header."); - var server = serverHeader.ToString(); - - logger.Debug("Proxying {Method} request to {Domain}{Path}", method, server, path); - - // Identity instances always initiate federation, so we have to make sure the instance knows about us. - // This also serves as a way to make sure the instance being requested actually exists. - await chatInstanceResolverService.ResolveChatInstanceAsync(serverHeader.ToString()); - - var resp = await requestSigningService.RequestAsync(method, server, path, acct.Id.ToString(), body); - return Ok(resp); - } -} \ No newline at end of file diff --git a/Foxchat.Identity/Controllers/UsersController.cs b/Foxchat.Identity/Controllers/UsersController.cs deleted file mode 100644 index 6302183..0000000 --- a/Foxchat.Identity/Controllers/UsersController.cs +++ /dev/null @@ -1,52 +0,0 @@ -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 -{ - [HttpGet("{id}")] - public async Task 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)); - } - - [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/Extensions/WebApplicationExtensions.cs b/Foxchat.Identity/Extensions/WebApplicationExtensions.cs index 63628ce..7597b43 100644 --- a/Foxchat.Identity/Extensions/WebApplicationExtensions.cs +++ b/Foxchat.Identity/Extensions/WebApplicationExtensions.cs @@ -1,4 +1,3 @@ -using Foxchat.Core.Middleware; using Foxchat.Identity.Middleware; namespace Foxchat.Identity.Extensions; @@ -9,15 +8,15 @@ public static class WebApplicationExtensions { return services .AddScoped() - .AddScoped() - .AddScoped(); + .AddScoped() + .AddScoped(); } public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app) { return app .UseMiddleware() - .UseMiddleware() - .UseMiddleware(); + .UseMiddleware() + .UseMiddleware(); } } diff --git a/Foxchat.Identity/Middleware/ClientAuthenticationMiddleware.cs b/Foxchat.Identity/Middleware/AuthenticationMiddleware.cs similarity index 92% rename from Foxchat.Identity/Middleware/ClientAuthenticationMiddleware.cs rename to Foxchat.Identity/Middleware/AuthenticationMiddleware.cs index 4ad5b2c..224e485 100644 --- a/Foxchat.Identity/Middleware/ClientAuthenticationMiddleware.cs +++ b/Foxchat.Identity/Middleware/AuthenticationMiddleware.cs @@ -8,7 +8,7 @@ using NodaTime; namespace Foxchat.Identity.Middleware; -public class ClientAuthenticationMiddleware( +public class AuthenticationMiddleware( IdentityContext db, IClock clock ) : IMiddleware @@ -16,7 +16,7 @@ public class ClientAuthenticationMiddleware( public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { var endpoint = ctx.GetEndpoint(); - var metadata = endpoint?.Metadata.GetMetadata(); + var metadata = endpoint?.Metadata.GetMetadata(); if (metadata == null) { @@ -81,4 +81,4 @@ public static class HttpContextExtensions } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class ClientAuthenticateAttribute : Attribute; +public class AuthenticateAttribute : Attribute; diff --git a/Foxchat.Identity/Middleware/ClientAuthorizationMiddleware.cs b/Foxchat.Identity/Middleware/AuthorizationMiddleware.cs similarity index 79% rename from Foxchat.Identity/Middleware/ClientAuthorizationMiddleware.cs rename to Foxchat.Identity/Middleware/AuthorizationMiddleware.cs index 2e6499d..46a2fe6 100644 --- a/Foxchat.Identity/Middleware/ClientAuthorizationMiddleware.cs +++ b/Foxchat.Identity/Middleware/AuthorizationMiddleware.cs @@ -1,11 +1,10 @@ using Foxchat.Core; using Foxchat.Identity.Database; -using Foxchat.Identity.Utils; using NodaTime; namespace Foxchat.Identity.Middleware; -public class ClientAuthorizationMiddleware( +public class AuthorizationMiddleware( IdentityContext db, IClock clock ) : IMiddleware @@ -22,10 +21,10 @@ public class ClientAuthorizationMiddleware( } var token = ctx.GetToken(); - if (token == null || token.Expires < clock.GetCurrentInstant()) + 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.Scopes).Any()) + throw new ApiError.Forbidden("This endpoint requires ungranted scopes.", attribute.Scopes.Except(token.Scopes)); await next(ctx); } diff --git a/Foxchat.Core/Middleware/ErrorHandlerMiddleware.cs b/Foxchat.Identity/Middleware/ErrorHandlerMiddleware.cs similarity index 95% rename from Foxchat.Core/Middleware/ErrorHandlerMiddleware.cs rename to Foxchat.Identity/Middleware/ErrorHandlerMiddleware.cs index 0f34dd1..6b8d7e5 100644 --- a/Foxchat.Core/Middleware/ErrorHandlerMiddleware.cs +++ b/Foxchat.Identity/Middleware/ErrorHandlerMiddleware.cs @@ -1,10 +1,11 @@ using System.Net; +using Foxchat.Core; using Foxchat.Core.Models.Http; -using Microsoft.AspNetCore.Http; using Newtonsoft.Json; +using ApiError = Foxchat.Core.ApiError; using HttpApiError = Foxchat.Core.Models.Http.ApiError; -namespace Foxchat.Core.Middleware; +namespace Foxchat.Identity.Middleware; public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware { @@ -22,8 +23,7 @@ public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware 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) 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..26188bb 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"; @@ -27,8 +24,4 @@ public static class OauthUtils return false; } } - - public static string[] ExpandScopes(this string[] scopes) => scopes.Contains("chat_client") - ? Scopes - : 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