Compare commits
No commits in common. "main" and "v0.0.1.proto" have entirely different histories.
main
...
v0.0.1.pro
61 changed files with 145 additions and 2290 deletions
|
@ -2,6 +2,3 @@
|
||||||
|
|
||||||
# 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,47 +1,3 @@
|
||||||
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
|
|
||||||
|
|
13
.idea/.idea.Foxchat/.idea/.gitignore
vendored
13
.idea/.idea.Foxchat/.idea/.gitignore
vendored
|
@ -1,13 +0,0 @@
|
||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Rider ignored files
|
|
||||||
/.idea.Foxchat.iml
|
|
||||||
/contentModel.xml
|
|
||||||
/modules.xml
|
|
||||||
/projectSettingsUpdater.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
|
@ -1 +0,0 @@
|
||||||
Foxchat
|
|
|
@ -1,4 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
|
||||||
</project>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="UserContentModel">
|
|
||||||
<attachedFolders />
|
|
||||||
<explicitIncludes />
|
|
||||||
<explicitExcludes />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="SqlDialectMappings">
|
|
||||||
<file url="file://$PROJECT_DIR$/Foxchat.Chat/Migrations/20240521132416_Init.cs" dialect="PostgreSQL" />
|
|
||||||
<file url="PROJECT" dialect="PostgreSQL" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
using Foxchat.Chat.Database;
|
|
||||||
using Foxchat.Chat.Database.Models;
|
|
||||||
using Foxchat.Chat.Middleware;
|
|
||||||
using Foxchat.Core.Extensions;
|
|
||||||
using Foxchat.Core.Federation;
|
|
||||||
using Foxchat.Core.Models.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using ApiError = Foxchat.Core.ApiError;
|
|
||||||
|
|
||||||
namespace Foxchat.Chat.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[ServerUnauthenticated]
|
|
||||||
[Route("/_fox/chat/hello")]
|
|
||||||
public class HelloController(
|
|
||||||
ILogger logger,
|
|
||||||
ChatContext db,
|
|
||||||
InstanceConfig config,
|
|
||||||
RequestSigningService requestSigningService)
|
|
||||||
: ControllerBase
|
|
||||||
{
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> Hello([FromBody] Hello.HelloRequest req)
|
|
||||||
{
|
|
||||||
var node = await requestSigningService.RequestAsync<Hello.NodeInfo>(HttpMethod.Get, req.Host,
|
|
||||||
"/_fox/ident/node");
|
|
||||||
|
|
||||||
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.");
|
|
||||||
|
|
||||||
var instance = await db.GetInstanceAsync();
|
|
||||||
db.IdentityInstances.Add(new IdentityInstance
|
|
||||||
{
|
|
||||||
Domain = req.Host,
|
|
||||||
BaseUrl = $"https://{req.Host}",
|
|
||||||
PublicKey = node.PublicKey
|
|
||||||
});
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return Ok(new Hello.HelloResponse(instance.PublicKey, config.Domain));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
namespace Foxchat.Chat.Database;
|
|
||||||
|
|
||||||
public abstract class BaseModel
|
|
||||||
{
|
|
||||||
public Ulid Id { get; init; } = Ulid.NewUlid();
|
|
||||||
}
|
|
|
@ -1,7 +1,5 @@
|
||||||
using Foxchat.Chat.Database.Models;
|
|
||||||
using Foxchat.Core;
|
using Foxchat.Core;
|
||||||
using Foxchat.Core.Database;
|
using Foxchat.Core.Database;
|
||||||
using Foxchat.Core.Extensions;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
|
@ -11,16 +9,10 @@ namespace Foxchat.Chat.Database;
|
||||||
public class ChatContext : IDatabaseContext
|
public class ChatContext : IDatabaseContext
|
||||||
{
|
{
|
||||||
private readonly NpgsqlDataSource _dataSource;
|
private readonly NpgsqlDataSource _dataSource;
|
||||||
private readonly ILoggerFactory? _loggerFactory;
|
|
||||||
|
|
||||||
public override DbSet<Instance> Instance { get; set; }
|
public override DbSet<Instance> Instance { get; set; }
|
||||||
public DbSet<IdentityInstance> IdentityInstances { get; set; }
|
|
||||||
public DbSet<User> Users { get; set; }
|
|
||||||
public DbSet<Guild> Guilds { get; set; }
|
|
||||||
public DbSet<Channel> Channels { get; set; }
|
|
||||||
public DbSet<Message> Messages { get; set; }
|
|
||||||
|
|
||||||
public ChatContext(InstanceConfig config, ILoggerFactory? loggerFactory)
|
public ChatContext(InstanceConfig config)
|
||||||
{
|
{
|
||||||
var connString = new NpgsqlConnectionStringBuilder(config.Database.Url)
|
var connString = new NpgsqlConnectionStringBuilder(config.Database.Url)
|
||||||
{
|
{
|
||||||
|
@ -31,14 +23,12 @@ public class ChatContext : IDatabaseContext
|
||||||
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString);
|
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString);
|
||||||
dataSourceBuilder.UseNodaTime();
|
dataSourceBuilder.UseNodaTime();
|
||||||
_dataSource = dataSourceBuilder.Build();
|
_dataSource = dataSourceBuilder.Build();
|
||||||
_loggerFactory = loggerFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
=> optionsBuilder
|
=> optionsBuilder
|
||||||
.UseNpgsql(_dataSource, o => o.UseNodaTime())
|
.UseNpgsql(_dataSource, o => o.UseNodaTime())
|
||||||
.UseSnakeCaseNamingConvention()
|
.UseSnakeCaseNamingConvention();
|
||||||
.UseLoggerFactory(_loggerFactory);
|
|
||||||
|
|
||||||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
|
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
|
||||||
{
|
{
|
||||||
|
@ -48,35 +38,20 @@ public class ChatContext : IDatabaseContext
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder.Entity<IdentityInstance>().HasIndex(i => i.Domain).IsUnique();
|
|
||||||
|
|
||||||
modelBuilder.Entity<User>().HasIndex(u => new { u.RemoteUserId, u.InstanceId }).IsUnique();
|
|
||||||
modelBuilder.Entity<User>().HasIndex(u => new { u.Username, u.InstanceId }).IsUnique();
|
|
||||||
|
|
||||||
modelBuilder.Entity<Guild>()
|
|
||||||
.HasOne(e => e.Owner)
|
|
||||||
.WithMany(e => e.OwnedGuilds)
|
|
||||||
.HasForeignKey(e => e.OwnerId)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
modelBuilder.Entity<User>()
|
|
||||||
.HasMany(e => e.Guilds)
|
|
||||||
.WithMany(e => e.Users);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReSharper disable once UnusedType.Global
|
|
||||||
public class DesignTimeIdentityContextFactory : IDesignTimeDbContextFactory<ChatContext>
|
public class DesignTimeIdentityContextFactory : IDesignTimeDbContextFactory<ChatContext>
|
||||||
{
|
{
|
||||||
public ChatContext CreateDbContext(string[] args)
|
public ChatContext CreateDbContext(string[] args)
|
||||||
{
|
{
|
||||||
// Read the configuration file
|
// Read the configuration file
|
||||||
var config = new ConfigurationBuilder()
|
var config = new ConfigurationBuilder()
|
||||||
.AddConfiguration("chat.ini")
|
.AddConfiguration("identity.ini")
|
||||||
.Build()
|
.Build()
|
||||||
// Get the configuration as our config class
|
// Get the configuration as our config class
|
||||||
.Get<InstanceConfig>() ?? new();
|
.Get<InstanceConfig>() ?? new();
|
||||||
|
|
||||||
return new ChatContext(config, null);
|
return new ChatContext(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
namespace Foxchat.Chat.Database.Models;
|
|
||||||
|
|
||||||
public class Channel : BaseModel
|
|
||||||
{
|
|
||||||
public Ulid GuildId { get; init; }
|
|
||||||
public Guild Guild { get; init; } = null!;
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
public string? Topic { get; set; }
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
namespace Foxchat.Chat.Database.Models;
|
|
||||||
|
|
||||||
public class Guild : BaseModel
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
public Ulid OwnerId { get; set; }
|
|
||||||
public User Owner { get; set; } = null!;
|
|
||||||
|
|
||||||
public List<User> Users { get; } = [];
|
|
||||||
public List<Channel> Channels { get; } = [];
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
namespace Foxchat.Chat.Database.Models;
|
|
||||||
|
|
||||||
public class IdentityInstance : BaseModel
|
|
||||||
{
|
|
||||||
public string Domain { get; init; } = null!;
|
|
||||||
public string BaseUrl { get; init; } = null!;
|
|
||||||
public string PublicKey { get; init; } = null!;
|
|
||||||
|
|
||||||
public InstanceStatus Status { get; set; } = InstanceStatus.Active;
|
|
||||||
public string? Reason { get; set; }
|
|
||||||
|
|
||||||
public enum InstanceStatus
|
|
||||||
{
|
|
||||||
Active,
|
|
||||||
Suspended,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
using NodaTime;
|
|
||||||
|
|
||||||
namespace Foxchat.Chat.Database.Models;
|
|
||||||
|
|
||||||
public class Message : BaseModel
|
|
||||||
{
|
|
||||||
public Ulid ChannelId { get; init; }
|
|
||||||
public Channel Channel { get; init; } = null!;
|
|
||||||
public Ulid AuthorId { get; init; }
|
|
||||||
public User Author { get; init; } = null!;
|
|
||||||
|
|
||||||
public string? Content { get; set; }
|
|
||||||
|
|
||||||
public Instant? UpdatedAt { get; set; }
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
using Foxchat.Core.Models;
|
|
||||||
using NodaTime;
|
|
||||||
|
|
||||||
namespace Foxchat.Chat.Database.Models;
|
|
||||||
|
|
||||||
public class User : BaseModel
|
|
||||||
{
|
|
||||||
public Ulid InstanceId { get; init; }
|
|
||||||
public IdentityInstance Instance { get; init; } = null!;
|
|
||||||
public string RemoteUserId { get; init; } = null!;
|
|
||||||
public string Username { get; init; } = null!;
|
|
||||||
|
|
||||||
public string? Avatar { get; set; }
|
|
||||||
public Instant LastFetchedAt { get; set; }
|
|
||||||
|
|
||||||
public List<Guild> Guilds { get; } = [];
|
|
||||||
public List<Guild> OwnedGuilds { get; } = [];
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
using Foxchat.Chat.Middleware;
|
|
||||||
using Foxchat.Chat.Services;
|
|
||||||
using Foxchat.Core.Middleware;
|
|
||||||
|
|
||||||
namespace Foxchat.Chat.Extensions;
|
|
||||||
|
|
||||||
public static class WebApplicationExtensions
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddCustomMiddleware(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
return services
|
|
||||||
.AddScoped<ErrorHandlerMiddleware>()
|
|
||||||
.AddScoped<ServerAuthenticationMiddleware>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app)
|
|
||||||
{
|
|
||||||
return app
|
|
||||||
.UseMiddleware<ErrorHandlerMiddleware>()
|
|
||||||
.UseMiddleware<ServerAuthenticationMiddleware>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IServiceCollection AddChatServices(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
return services
|
|
||||||
.AddScoped<UserResolverService>();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
using Foxchat.Chat.Database;
|
|
||||||
using Foxchat.Chat.Database.Models;
|
|
||||||
using Foxchat.Core;
|
|
||||||
using Foxchat.Core.Extensions;
|
|
||||||
using Foxchat.Core.Federation;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Foxchat.Chat.Middleware;
|
|
||||||
|
|
||||||
public class ServerAuthenticationMiddleware(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<ServerUnauthenticatedAttribute>();
|
|
||||||
if (metadata != null)
|
|
||||||
{
|
|
||||||
await next(ctx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.ExtractRequestData(out var signature, out var domain, out var signatureData))
|
|
||||||
throw new ApiError.IncomingFederationError("This endpoint requires signed requests.");
|
|
||||||
|
|
||||||
var instance = await GetInstanceAsync(domain);
|
|
||||||
|
|
||||||
if (!requestSigningService.VerifySignature(instance.PublicKey, signature, signatureData))
|
|
||||||
throw new ApiError.IncomingFederationError("Signature is not valid.");
|
|
||||||
|
|
||||||
ctx.SetSignature(instance, signatureData);
|
|
||||||
|
|
||||||
await next(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IdentityInstance> GetInstanceAsync(string domain)
|
|
||||||
{
|
|
||||||
return await db.IdentityInstances.FirstOrDefaultAsync(i => i.Domain == domain)
|
|
||||||
?? throw new ApiError.IncomingFederationError("Remote instance is not known.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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)]
|
|
||||||
public class ServerUnauthenticatedAttribute : Attribute;
|
|
||||||
|
|
||||||
public static class HttpContextExtensions
|
|
||||||
{
|
|
||||||
private const string Key = "instance";
|
|
||||||
|
|
||||||
public static void SetSignature(this HttpContext ctx, IdentityInstance instance, SignatureData data)
|
|
||||||
{
|
|
||||||
ctx.Items.Add(Key, (instance, data));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static (IdentityInstance?, SignatureData?) GetSignature(this HttpContext ctx)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var obj = ctx.GetSignatureOrThrow();
|
|
||||||
return (obj.Item1, obj.Item2);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return (null, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static (IdentityInstance, SignatureData) GetSignatureOrThrow(this HttpContext ctx)
|
|
||||||
{
|
|
||||||
if (!ctx.Items.TryGetValue(Key, out var obj))
|
|
||||||
throw new ApiError.AuthenticationError("No instance in HttpContext");
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
325
Foxchat.Chat/Migrations/20240521132416_Init.Designer.cs
generated
325
Foxchat.Chat/Migrations/20240521132416_Init.Designer.cs
generated
|
@ -1,325 +0,0 @@
|
||||||
// <auto-generated />
|
|
||||||
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("20240521132416_Init")]
|
|
||||||
partial class Init
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<Guid>("GuildId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("guild_id");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("name");
|
|
||||||
|
|
||||||
b.Property<string>("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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("name");
|
|
||||||
|
|
||||||
b.Property<Guid>("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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<string>("BaseUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("base_url");
|
|
||||||
|
|
||||||
b.Property<string>("Domain")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("domain");
|
|
||||||
|
|
||||||
b.Property<string>("PublicKey")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("public_key");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("reason");
|
|
||||||
|
|
||||||
b.Property<int>("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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<Guid>("AuthorId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("author_id");
|
|
||||||
|
|
||||||
b.Property<Guid>("ChannelId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("channel_id");
|
|
||||||
|
|
||||||
b.Property<string>("Content")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("content");
|
|
||||||
|
|
||||||
b.Property<Instant?>("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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<string>("Avatar")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("avatar");
|
|
||||||
|
|
||||||
b.Property<Guid>("InstanceId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("instance_id");
|
|
||||||
|
|
||||||
b.Property<string>("RemoteUserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("remote_user_id");
|
|
||||||
|
|
||||||
b.Property<string>("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<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("PrivateKey")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("private_key");
|
|
||||||
|
|
||||||
b.Property<string>("PublicKey")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("public_key");
|
|
||||||
|
|
||||||
b.HasKey("Id")
|
|
||||||
.HasName("pk_instance");
|
|
||||||
|
|
||||||
b.ToTable("instance", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GuildUser", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("GuildsId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("guilds_id");
|
|
||||||
|
|
||||||
b.Property<Guid>("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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,228 +0,0 @@
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using NodaTime;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Foxchat.Chat.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class Init : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "identity_instances",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
domain = table.Column<string>(type: "text", nullable: false),
|
|
||||||
base_url = table.Column<string>(type: "text", nullable: false),
|
|
||||||
public_key = table.Column<string>(type: "text", nullable: false),
|
|
||||||
status = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
reason = table.Column<string>(type: "text", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("pk_identity_instances", x => x.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "instance",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
public_key = table.Column<string>(type: "text", nullable: false),
|
|
||||||
private_key = table.Column<string>(type: "text", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("pk_instance", x => x.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "users",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
instance_id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
remote_user_id = table.Column<string>(type: "text", nullable: false),
|
|
||||||
username = table.Column<string>(type: "text", nullable: false),
|
|
||||||
avatar = table.Column<string>(type: "text", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("pk_users", x => x.id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "fk_users_identity_instances_instance_id",
|
|
||||||
column: x => x.instance_id,
|
|
||||||
principalTable: "identity_instances",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "guilds",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
owner_id = table.Column<Guid>(type: "uuid", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("pk_guilds", x => x.id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "fk_guilds_users_owner_id",
|
|
||||||
column: x => x.owner_id,
|
|
||||||
principalTable: "users",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "channels",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
guild_id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
topic = table.Column<string>(type: "text", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("pk_channels", x => x.id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "fk_channels_guilds_guild_id",
|
|
||||||
column: x => x.guild_id,
|
|
||||||
principalTable: "guilds",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "guild_user",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
guilds_id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
users_id = table.Column<Guid>(type: "uuid", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("pk_guild_user", x => new { x.guilds_id, x.users_id });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "fk_guild_user_guilds_guilds_id",
|
|
||||||
column: x => x.guilds_id,
|
|
||||||
principalTable: "guilds",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "fk_guild_user_users_users_id",
|
|
||||||
column: x => x.users_id,
|
|
||||||
principalTable: "users",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "messages",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
channel_id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
author_id = table.Column<Guid>(type: "uuid", nullable: false),
|
|
||||||
content = table.Column<string>(type: "text", nullable: true),
|
|
||||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("pk_messages", x => x.id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "fk_messages_channels_channel_id",
|
|
||||||
column: x => x.channel_id,
|
|
||||||
principalTable: "channels",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "fk_messages_users_author_id",
|
|
||||||
column: x => x.author_id,
|
|
||||||
principalTable: "users",
|
|
||||||
principalColumn: "id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "ix_channels_guild_id",
|
|
||||||
table: "channels",
|
|
||||||
column: "guild_id");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "ix_guild_user_users_id",
|
|
||||||
table: "guild_user",
|
|
||||||
column: "users_id");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "ix_guilds_owner_id",
|
|
||||||
table: "guilds",
|
|
||||||
column: "owner_id");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "ix_identity_instances_domain",
|
|
||||||
table: "identity_instances",
|
|
||||||
column: "domain",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "ix_messages_author_id",
|
|
||||||
table: "messages",
|
|
||||||
column: "author_id");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "ix_messages_channel_id",
|
|
||||||
table: "messages",
|
|
||||||
column: "channel_id");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "ix_users_instance_id",
|
|
||||||
table: "users",
|
|
||||||
column: "instance_id");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "ix_users_remote_user_id_instance_id",
|
|
||||||
table: "users",
|
|
||||||
columns: new[] { "remote_user_id", "instance_id" },
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
// EF Core doesn't support creating indexes on arbitrary expressions, so we have to create it manually.
|
|
||||||
migrationBuilder.Sql("CREATE UNIQUE INDEX ix_users_username_instance_id ON users (lower(username), instance_id)");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "guild_user");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "instance");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "messages");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "channels");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "guilds");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "users");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "identity_instances");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,329 +0,0 @@
|
||||||
// <auto-generated />
|
|
||||||
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
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<Guid>("GuildId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("guild_id");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("name");
|
|
||||||
|
|
||||||
b.Property<string>("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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("name");
|
|
||||||
|
|
||||||
b.Property<Guid>("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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<string>("BaseUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("base_url");
|
|
||||||
|
|
||||||
b.Property<string>("Domain")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("domain");
|
|
||||||
|
|
||||||
b.Property<string>("PublicKey")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("public_key");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("reason");
|
|
||||||
|
|
||||||
b.Property<int>("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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<Guid>("AuthorId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("author_id");
|
|
||||||
|
|
||||||
b.Property<Guid>("ChannelId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("channel_id");
|
|
||||||
|
|
||||||
b.Property<string>("Content")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("content");
|
|
||||||
|
|
||||||
b.Property<Instant?>("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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<string>("Avatar")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("avatar");
|
|
||||||
|
|
||||||
b.Property<Guid>("InstanceId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("instance_id");
|
|
||||||
|
|
||||||
b.Property<Instant>("LastFetchedAt")
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasColumnName("last_fetched_at");
|
|
||||||
|
|
||||||
b.Property<string>("RemoteUserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("remote_user_id");
|
|
||||||
|
|
||||||
b.Property<string>("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<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("PrivateKey")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("private_key");
|
|
||||||
|
|
||||||
b.Property<string>("PublicKey")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("public_key");
|
|
||||||
|
|
||||||
b.HasKey("Id")
|
|
||||||
.HasName("pk_instance");
|
|
||||||
|
|
||||||
b.ToTable("instance", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GuildUser", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("GuildsId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("guilds_id");
|
|
||||||
|
|
||||||
b.Property<Guid>("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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using NodaTime;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Foxchat.Chat.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddLastFetchedAtToUsers : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<Instant>(
|
|
||||||
name: "last_fetched_at",
|
|
||||||
table: "users",
|
|
||||||
type: "timestamp with time zone",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: NodaTime.Instant.FromUnixTimeTicks(0L));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "last_fetched_at",
|
|
||||||
table: "users");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,326 +0,0 @@
|
||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Foxchat.Chat.Database;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using NodaTime;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Foxchat.Chat.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(ChatContext))]
|
|
||||||
partial class ChatContextModelSnapshot : ModelSnapshot
|
|
||||||
{
|
|
||||||
protected override void BuildModel(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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<Guid>("GuildId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("guild_id");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("name");
|
|
||||||
|
|
||||||
b.Property<string>("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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("name");
|
|
||||||
|
|
||||||
b.Property<Guid>("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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<string>("BaseUrl")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("base_url");
|
|
||||||
|
|
||||||
b.Property<string>("Domain")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("domain");
|
|
||||||
|
|
||||||
b.Property<string>("PublicKey")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("public_key");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("reason");
|
|
||||||
|
|
||||||
b.Property<int>("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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<Guid>("AuthorId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("author_id");
|
|
||||||
|
|
||||||
b.Property<Guid>("ChannelId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("channel_id");
|
|
||||||
|
|
||||||
b.Property<string>("Content")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("content");
|
|
||||||
|
|
||||||
b.Property<Instant?>("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<Guid>("Id")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
b.Property<string>("Avatar")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("avatar");
|
|
||||||
|
|
||||||
b.Property<Guid>("InstanceId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("instance_id");
|
|
||||||
|
|
||||||
b.Property<Instant>("LastFetchedAt")
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasColumnName("last_fetched_at");
|
|
||||||
|
|
||||||
b.Property<string>("RemoteUserId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("remote_user_id");
|
|
||||||
|
|
||||||
b.Property<string>("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<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasColumnName("id");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("PrivateKey")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("private_key");
|
|
||||||
|
|
||||||
b.Property<string>("PublicKey")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("public_key");
|
|
||||||
|
|
||||||
b.HasKey("Id")
|
|
||||||
.HasName("pk_instance");
|
|
||||||
|
|
||||||
b.ToTable("instance", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("GuildUser", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("GuildsId")
|
|
||||||
.HasColumnType("uuid")
|
|
||||||
.HasColumnName("guilds_id");
|
|
||||||
|
|
||||||
b.Property<Guid>("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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,28 +3,16 @@ using Serilog;
|
||||||
using Foxchat.Core;
|
using Foxchat.Core;
|
||||||
using Foxchat.Chat;
|
using Foxchat.Chat;
|
||||||
using Foxchat.Chat.Database;
|
using Foxchat.Chat.Database;
|
||||||
using Foxchat.Chat.Extensions;
|
|
||||||
using Foxchat.Core.Extensions;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
var config = builder.AddConfiguration<InstanceConfig>("chat.ini");
|
var config = builder.AddConfiguration<InstanceConfig>("chat.ini");
|
||||||
|
|
||||||
builder.AddSerilog();
|
builder.AddSerilog(config.LogEventLevel);
|
||||||
|
|
||||||
await BuildInfo.ReadBuildInfo();
|
await BuildInfo.ReadBuildInfo();
|
||||||
Log.Information("Starting Foxchat.Chat {Version} ({Hash})", BuildInfo.Version, BuildInfo.Hash);
|
Log.Information("Starting Foxchat.Chat {Version} ({Hash})", BuildInfo.Version, BuildInfo.Hash);
|
||||||
|
|
||||||
// Set the default converter to snake case as we use it in a couple places.
|
|
||||||
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
ContractResolver = new DefaultContractResolver
|
|
||||||
{
|
|
||||||
NamingStrategy = new SnakeCaseNamingStrategy()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddControllers()
|
.AddControllers()
|
||||||
.AddNewtonsoftJson(options =>
|
.AddNewtonsoftJson(options =>
|
||||||
|
@ -35,8 +23,6 @@ builder.Services
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddCoreServices<ChatContext>()
|
.AddCoreServices<ChatContext>()
|
||||||
.AddChatServices()
|
|
||||||
.AddCustomMiddleware()
|
|
||||||
.AddEndpointsApiExplorer()
|
.AddEndpointsApiExplorer()
|
||||||
.AddSwaggerGen();
|
.AddSwaggerGen();
|
||||||
|
|
||||||
|
@ -47,11 +33,12 @@ app.UseRouting();
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI();
|
app.UseSwaggerUI();
|
||||||
app.UseCors();
|
app.UseCors();
|
||||||
app.UseCustomMiddleware();
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
await using (var context = scope.ServiceProvider.GetRequiredService<ChatContext>())
|
using (var context = scope.ServiceProvider.GetRequiredService<ChatContext>())
|
||||||
{
|
{
|
||||||
Log.Information("Initializing instance keypair...");
|
Log.Information("Initializing instance keypair...");
|
||||||
if (await context.InitializeInstanceAsync())
|
if (await context.InitializeInstanceAsync())
|
||||||
|
|
|
@ -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<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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,6 +2,9 @@ Host = localhost
|
||||||
Port = 7610
|
Port = 7610
|
||||||
Domain = chat.fox.localhost
|
Domain = chat.fox.localhost
|
||||||
|
|
||||||
|
; The level to log things at. Valid settings: Verbose, Debug, Information, Warning, Error, Fatal
|
||||||
|
LogEventLevel = Debug
|
||||||
|
|
||||||
[Database]
|
[Database]
|
||||||
; The database URL in ADO.NET format.
|
; The database URL in ADO.NET format.
|
||||||
Url = "Host=localhost;Database=foxchat_cs_chat;Username=foxchat;Password=password"
|
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
|
Timeout = 5
|
||||||
; The maximum number of open connections. Defaults to 50.
|
; The maximum number of open connections. Defaults to 50.
|
||||||
MaxPoolSize = 500
|
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
|
|
|
@ -11,7 +11,9 @@ public class CoreConfig
|
||||||
|
|
||||||
public string Address => $"{(Secure ? "https" : "http")}://{Host}:{Port}";
|
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 DatabaseConfig Database { get; set; } = new();
|
||||||
|
|
||||||
public class DatabaseConfig
|
public class DatabaseConfig
|
||||||
|
@ -20,11 +22,4 @@ public class CoreConfig
|
||||||
public int? Timeout { get; set; }
|
public int? Timeout { get; set; }
|
||||||
public int? MaxPoolSize { 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,11 +16,11 @@ public abstract class IDatabaseContext : DbContext
|
||||||
var publicKey = rsa.ExportRSAPublicKeyPem();
|
var publicKey = rsa.ExportRSAPublicKeyPem();
|
||||||
var privateKey = rsa.ExportRSAPrivateKeyPem();
|
var privateKey = rsa.ExportRSAPrivateKeyPem();
|
||||||
|
|
||||||
Instance.Add(new Instance
|
await Instance.AddAsync(new Instance
|
||||||
{
|
{
|
||||||
PublicKey = publicKey!,
|
PublicKey = publicKey!,
|
||||||
PrivateKey = privateKey!,
|
PrivateKey = privateKey!,
|
||||||
});
|
}, ct);
|
||||||
|
|
||||||
await SaveChangesAsync(ct);
|
await SaveChangesAsync(ct);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using Foxchat.Core.Federation;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
signature = null;
|
|
||||||
domain = null;
|
|
||||||
data = null;
|
|
||||||
|
|
||||||
if (!ctx.Request.Headers.TryGetValue(RequestSigningService.SIGNATURE_HEADER, out var encodedSignature))
|
|
||||||
return false;
|
|
||||||
if (!ctx.Request.Headers.TryGetValue(RequestSigningService.DATE_HEADER, out var date))
|
|
||||||
return false;
|
|
||||||
if (!ctx.Request.Headers.TryGetValue(RequestSigningService.SERVER_HEADER, out var server))
|
|
||||||
return false;
|
|
||||||
var time = RequestSigningService.ParseTime(date.ToString());
|
|
||||||
string? userId = null;
|
|
||||||
if (ctx.Request.Headers.TryGetValue(RequestSigningService.USER_HEADER, out var userIdHeader))
|
|
||||||
userId = userIdHeader;
|
|
||||||
var host = ctx.Request.Headers.Host.ToString();
|
|
||||||
|
|
||||||
signature = encodedSignature.ToString();
|
|
||||||
domain = server.ToString();
|
|
||||||
data = new SignatureData(
|
|
||||||
time,
|
|
||||||
host,
|
|
||||||
ctx.Request.Path,
|
|
||||||
(int?)ctx.Request.Headers.ContentLength,
|
|
||||||
userId
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
using Foxchat.Core.Models;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
|
|
||||||
|
@ -14,6 +15,14 @@ public partial class RequestSigningService
|
||||||
public const string SIGNATURE_HEADER = "X-Foxchat-Signature";
|
public const string SIGNATURE_HEADER = "X-Foxchat-Signature";
|
||||||
public const string USER_HEADER = "X-Foxchat-User";
|
public const string USER_HEADER = "X-Foxchat-User";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerSettings _jsonSerializerSettings = new()
|
||||||
|
{
|
||||||
|
ContractResolver = new DefaultContractResolver
|
||||||
|
{
|
||||||
|
NamingStrategy = new SnakeCaseNamingStrategy()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public async Task<T> RequestAsync<T>(HttpMethod method, string domain, string requestPath, string? userId = null, object? body = null)
|
public async Task<T> RequestAsync<T>(HttpMethod method, string domain, string requestPath, string? userId = null, object? body = null)
|
||||||
{
|
{
|
||||||
var request = BuildHttpRequest(method, domain, requestPath, userId, body);
|
var request = BuildHttpRequest(method, domain, requestPath, userId, body);
|
||||||
|
@ -21,17 +30,17 @@ public partial class RequestSigningService
|
||||||
if (!resp.IsSuccessStatusCode)
|
if (!resp.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var error = await resp.Content.ReadAsStringAsync();
|
var error = await resp.Content.ReadAsStringAsync();
|
||||||
throw new ApiError.OutgoingFederationError($"Request to {domain}{requestPath} returned an error", JsonConvert.DeserializeObject<Models.Http.ApiError>(error));
|
throw new ApiError.OutgoingFederationError($"Request to {domain}{requestPath} returned an error", DeserializeObject<Models.ApiError>(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
var bodyString = await resp.Content.ReadAsStringAsync();
|
var bodyString = await resp.Content.ReadAsStringAsync();
|
||||||
return JsonConvert.DeserializeObject<T>(bodyString)
|
return DeserializeObject<T>(bodyString)
|
||||||
?? throw new ApiError.OutgoingFederationError($"Request to {domain}{requestPath} returned invalid response body");
|
?? throw new ApiError.OutgoingFederationError($"Request to {domain}{requestPath} returned invalid response body");
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpRequestMessage BuildHttpRequest(HttpMethod method, string domain, string requestPath, string? userId = null, object? bodyData = null)
|
private HttpRequestMessage BuildHttpRequest(HttpMethod method, string domain, string requestPath, string? userId = null, object? bodyData = null)
|
||||||
{
|
{
|
||||||
var body = bodyData != null ? JsonConvert.SerializeObject(bodyData) : null;
|
var body = bodyData != null ? SerializeObject(bodyData) : null;
|
||||||
|
|
||||||
var now = _clock.GetCurrentInstant();
|
var now = _clock.GetCurrentInstant();
|
||||||
var url = $"https://{domain}{requestPath}";
|
var url = $"https://{domain}{requestPath}";
|
||||||
|
@ -50,4 +59,7 @@ public partial class RequestSigningService
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string SerializeObject(object data) => JsonConvert.SerializeObject(data, _jsonSerializerSettings);
|
||||||
|
public static T? DeserializeObject<T>(string data) => JsonConvert.DeserializeObject<T>(data, _jsonSerializerSettings);
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,21 +31,23 @@ 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, string dateHeader, string host, string requestPath, int? contentLength, string? userId)
|
||||||
{
|
{
|
||||||
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();
|
var rsa = RSA.Create();
|
||||||
rsa.ImportFromPem(publicKey);
|
rsa.ImportFromPem(publicKey);
|
||||||
|
|
||||||
var plaintext = GeneratePlaintext(data);
|
var now = _clock.GetCurrentInstant();
|
||||||
|
var time = ParseTime(dateHeader);
|
||||||
|
if ((now + Duration.FromMinutes(1)) < time)
|
||||||
|
{
|
||||||
|
throw new ApiError.IncomingFederationError("Request was made in the future");
|
||||||
|
}
|
||||||
|
else if ((now - Duration.FromMinutes(1)) > time)
|
||||||
|
{
|
||||||
|
throw new ApiError.IncomingFederationError("Request was made too long ago");
|
||||||
|
}
|
||||||
|
|
||||||
|
var plaintext = GeneratePlaintext(new SignatureData(time, host, requestPath, contentLength, userId));
|
||||||
var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
|
var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
|
||||||
var hash = SHA256.HashData(plaintextBytes);
|
var hash = SHA256.HashData(plaintextBytes);
|
||||||
|
|
||||||
|
@ -69,9 +71,7 @@ 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 =
|
private static readonly InstantPattern _pattern = InstantPattern.Create("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.GetCultureInfo("en-US"));
|
||||||
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();
|
private static Instant ParseTime(string header) => _pattern.Parse(header).GetValueOrThrow();
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,4 @@ public record SignatureData(
|
||||||
string RequestPath,
|
string RequestPath,
|
||||||
int? ContentLength,
|
int? ContentLength,
|
||||||
string? UserId
|
string? UserId
|
||||||
)
|
);
|
||||||
{
|
|
||||||
public static readonly SignatureData Empty = new(
|
|
||||||
Instant.MinValue,
|
|
||||||
string.Empty,
|
|
||||||
string.Empty,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,31 +2,31 @@ using System.Net;
|
||||||
|
|
||||||
namespace Foxchat.Core;
|
namespace Foxchat.Core;
|
||||||
|
|
||||||
public class FoxchatError(string message, Exception? inner = null) : Exception(message)
|
public class FoxchatError(string message) : Exception(message)
|
||||||
{
|
{
|
||||||
public Exception? Inner => inner;
|
public class DatabaseError(string message) : FoxchatError(message);
|
||||||
|
public class UnknownEntityError(Type entityType) : FoxchatError($"Entity of type {entityType.Name} not found");
|
||||||
public class DatabaseError(string message, Exception? inner = null) : FoxchatError(message, inner);
|
|
||||||
public class UnknownEntityError(Type entityType, Exception? inner = null) : FoxchatError($"Entity of type {entityType.Name} not found", inner);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ApiError(string message, HttpStatusCode? statusCode = null) : FoxchatError(message)
|
public class ApiError(string message, HttpStatusCode? statusCode = null) : FoxchatError(message)
|
||||||
{
|
{
|
||||||
public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError;
|
public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError;
|
||||||
|
|
||||||
public class Unauthorized(string message) : ApiError(message, statusCode: HttpStatusCode.Unauthorized);
|
public class Unauthorized(string message) : ApiError(message, statusCode: HttpStatusCode.Unauthorized);
|
||||||
|
|
||||||
public class Forbidden(string message, IEnumerable<string>? scopes = null) : ApiError(message, statusCode: HttpStatusCode.Forbidden)
|
public class Forbidden(string message, IEnumerable<string>? scopes = null) : ApiError(message, statusCode: HttpStatusCode.Forbidden)
|
||||||
{
|
{
|
||||||
public readonly string[] Scopes = scopes?.ToArray() ?? [];
|
public readonly string[] Scopes = scopes?.ToArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BadRequest(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
public class BadRequest(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
||||||
public class NotFound(string message) : ApiError(message, statusCode: HttpStatusCode.NotFound);
|
|
||||||
public class IncomingFederationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
public class IncomingFederationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
public class OutgoingFederationError(
|
public class OutgoingFederationError(
|
||||||
string message, Models.Http.ApiError? innerError = null
|
string message, Models.ApiError? innerError = null
|
||||||
) : ApiError(message, statusCode: HttpStatusCode.InternalServerError)
|
) : ApiError(message, statusCode: HttpStatusCode.InternalServerError)
|
||||||
{
|
{
|
||||||
public Models.Http.ApiError? InnerError => innerError;
|
public Models.ApiError? InnerError => innerError;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AuthenticationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
public class AuthenticationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest);
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
using System.Net;
|
|
||||||
using Foxchat.Core.Models.Http;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using HttpApiError = Foxchat.Core.Models.Http.ApiError;
|
|
||||||
|
|
||||||
namespace Foxchat.Core.Middleware;
|
|
||||||
|
|
||||||
public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware
|
|
||||||
{
|
|
||||||
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await next(ctx);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
var type = e.TargetSite?.DeclaringType ?? typeof(ErrorHandlerMiddleware);
|
|
||||||
var typeName = e.TargetSite?.DeclaringType?.FullName ?? "<unknown>";
|
|
||||||
var logger = baseLogger.ForContext(type);
|
|
||||||
|
|
||||||
if (ctx.Response.HasStarted)
|
|
||||||
{
|
|
||||||
logger.Error(e, "Error in {ClassName} ({Path}) after response started being sent", typeName,
|
|
||||||
ctx.Request.Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e is ApiError ae)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = (int)ae.StatusCode;
|
|
||||||
ctx.Response.Headers.RequestId = ctx.TraceIdentifier;
|
|
||||||
ctx.Response.ContentType = "application/json; charset=utf-8";
|
|
||||||
if (ae is ApiError.OutgoingFederationError ofe)
|
|
||||||
{
|
|
||||||
await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError
|
|
||||||
{
|
|
||||||
Status = (int)ofe.StatusCode,
|
|
||||||
Code = ErrorCode.OutgoingFederationError,
|
|
||||||
Message = ofe.Message,
|
|
||||||
OriginalError = ofe.InnerError
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (ae is ApiError.Forbidden fe)
|
|
||||||
{
|
|
||||||
await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError
|
|
||||||
{
|
|
||||||
Status = (int)fe.StatusCode,
|
|
||||||
Code = ErrorCode.Forbidden,
|
|
||||||
Message = fe.Message,
|
|
||||||
Scopes = fe.Scopes.Length > 0 ? fe.Scopes : null
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError
|
|
||||||
{
|
|
||||||
Status = (int)ae.StatusCode,
|
|
||||||
Code = ErrorCode.GenericApiError,
|
|
||||||
Message = ae.Message,
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e is FoxchatError fce)
|
|
||||||
{
|
|
||||||
logger.Error(fce.Inner ?? fce, "Exception in {ClassName} ({Path})", typeName, ctx.Request.Path);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logger.Error(e, "Exception in {ClassName} ({Path})", typeName, ctx.Request.Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
|
||||||
ctx.Response.Headers.RequestId = ctx.TraceIdentifier;
|
|
||||||
ctx.Response.ContentType = "application/json; charset=utf-8";
|
|
||||||
await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError
|
|
||||||
{
|
|
||||||
Status = (int)HttpStatusCode.InternalServerError,
|
|
||||||
Code = ErrorCode.InternalServerError,
|
|
||||||
Message = "Internal server error",
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
17
Foxchat.Core/Models/ApiError.cs
Normal file
17
Foxchat.Core/Models/ApiError.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
namespace Foxchat.Core.Models;
|
||||||
|
|
||||||
|
public record ApiError(int Status, ErrorCode Code, string Message);
|
||||||
|
|
||||||
|
public enum ErrorCode
|
||||||
|
{
|
||||||
|
INTERNAL_SERVER_ERROR,
|
||||||
|
OBJECT_NOT_FOUND,
|
||||||
|
INVALID_SERVER,
|
||||||
|
INVALID_HEADER,
|
||||||
|
INVALID_DATE,
|
||||||
|
INVALID_SIGNATURE,
|
||||||
|
MISSING_SIGNATURE,
|
||||||
|
GUILD_NOT_FOUND,
|
||||||
|
UNAUTHORIZED,
|
||||||
|
INVALID_REST_EVENT,
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
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,28 +0,0 @@
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Converters;
|
|
||||||
|
|
||||||
namespace Foxchat.Core.Models.Http;
|
|
||||||
|
|
||||||
public record ApiError
|
|
||||||
{
|
|
||||||
public required int Status { get; init; }
|
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
|
||||||
public required ErrorCode Code { get; init; }
|
|
||||||
public required string Message { get; init; }
|
|
||||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
|
||||||
public ApiError? OriginalError { get; init; }
|
|
||||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
|
||||||
public string[]? Scopes { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ErrorCode
|
|
||||||
{
|
|
||||||
InternalServerError,
|
|
||||||
Unauthorized,
|
|
||||||
Forbidden,
|
|
||||||
BadRequest,
|
|
||||||
OutgoingFederationError,
|
|
||||||
AuthenticationError,
|
|
||||||
// TODO: more specific API error codes
|
|
||||||
GenericApiError,
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
namespace Foxchat.Core.Models.Http;
|
namespace Foxchat.Core.Models.Http;
|
||||||
|
|
||||||
public static class AppsApi
|
public static class Apps
|
||||||
{
|
{
|
||||||
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);
|
|
@ -1,6 +0,0 @@
|
||||||
namespace Foxchat.Core.Models.Http;
|
|
||||||
|
|
||||||
public static class GuildsApi
|
|
||||||
{
|
|
||||||
public record CreateGuildRequest(string Name);
|
|
||||||
}
|
|
|
@ -4,6 +4,6 @@ public static class Hello
|
||||||
{
|
{
|
||||||
public record HelloRequest(string Host);
|
public record HelloRequest(string Host);
|
||||||
public record HelloResponse(string PublicKey, string Host);
|
public record HelloResponse(string PublicKey, string Host);
|
||||||
public record NodeInfo(NodeSoftware Software, string PublicKey);
|
public record NodeInfo(string Software, string PublicKey);
|
||||||
public record NodeSoftware(string Name, string? Version);
|
public record NodeSoftware(string Name, string? Version);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -7,32 +7,30 @@ using NodaTime;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Serilog.Events;
|
using Serilog.Events;
|
||||||
|
|
||||||
namespace Foxchat.Core.Extensions;
|
namespace Foxchat.Core;
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds Serilog to this service collection. This method also initializes Serilog so it should be called as early as possible, before any log calls.
|
/// Adds Serilog to this service collection. This method also initializes Serilog so it should be called as early as possible, before any log calls.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void AddSerilog(this WebApplicationBuilder builder)
|
public static void AddSerilog(this WebApplicationBuilder builder, LogEventLevel level)
|
||||||
{
|
{
|
||||||
var config = builder.Configuration.Get<CoreConfig>() ?? new();
|
var config = builder.Configuration.Get<CoreConfig>() ?? new();
|
||||||
|
|
||||||
var logCfg = new LoggerConfiguration()
|
var logCfg = new LoggerConfiguration()
|
||||||
.Enrich.FromLogContext()
|
.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.
|
// 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", 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.Hosting", LogEventLevel.Warning)
|
||||||
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning)
|
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning)
|
||||||
.MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning)
|
.MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning)
|
||||||
.WriteTo.Console();
|
.WriteTo.Console();
|
||||||
|
|
||||||
if (config.Logging.SeqLogUrl != null)
|
if (config.SeqLogUrl != null)
|
||||||
logCfg.WriteTo.Seq(config.Logging.SeqLogUrl, restrictedToMinimumLevel: LogEventLevel.Verbose);
|
logCfg.WriteTo.Seq(config.SeqLogUrl, restrictedToMinimumLevel: LogEventLevel.Verbose);
|
||||||
|
|
||||||
Log.Logger = logCfg.CreateLogger();
|
Log.Logger = logCfg.CreateLogger();
|
||||||
|
|
||||||
|
@ -56,9 +54,9 @@ public static class ServiceCollectionExtensions
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T AddConfiguration<T>(this WebApplicationBuilder builder, string? configFile = null)
|
public static T AddConfiguration<T>(this WebApplicationBuilder builder, string? configFile = null) where T : class, new()
|
||||||
where T : class, new()
|
|
||||||
{
|
{
|
||||||
|
|
||||||
builder.Configuration.Sources.Clear();
|
builder.Configuration.Sources.Clear();
|
||||||
builder.Configuration.AddConfiguration(configFile);
|
builder.Configuration.AddConfiguration(configFile);
|
||||||
|
|
||||||
|
@ -78,4 +76,4 @@ public static class ServiceCollectionExtensions
|
||||||
.AddIniFile(file, optional: false, reloadOnChange: true)
|
.AddIniFile(file, optional: false, reloadOnChange: true)
|
||||||
.AddEnvironmentVariables();
|
.AddEnvironmentVariables();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,9 +6,9 @@ using Foxchat.Identity.Database.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace Foxchat.Identity.Middleware;
|
namespace Foxchat.Identity.Authorization;
|
||||||
|
|
||||||
public class ClientAuthenticationMiddleware(
|
public class AuthenticationMiddleware(
|
||||||
IdentityContext db,
|
IdentityContext db,
|
||||||
IClock clock
|
IClock clock
|
||||||
) : IMiddleware
|
) : IMiddleware
|
||||||
|
@ -16,7 +16,7 @@ public class ClientAuthenticationMiddleware(
|
||||||
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<ClientAuthenticateAttribute>();
|
var metadata = endpoint?.Metadata.GetMetadata<AuthenticateAttribute>();
|
||||||
|
|
||||||
if (metadata == null)
|
if (metadata == null)
|
||||||
{
|
{
|
||||||
|
@ -71,14 +71,7 @@ public static class HttpContextExtensions
|
||||||
return token as Token;
|
return token as Token;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Application GetApplicationOrThrow(this HttpContext context)
|
|
||||||
{
|
|
||||||
var token = context.GetToken();
|
|
||||||
if (token is not { Account: null }) throw new ApiError.Forbidden("This endpoint requires a client token.");
|
|
||||||
return token.Application;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||||
public class ClientAuthenticateAttribute : Attribute;
|
public class AuthenticateAttribute : Attribute;
|
|
@ -1,11 +1,11 @@
|
||||||
|
using System.Net;
|
||||||
using Foxchat.Core;
|
using Foxchat.Core;
|
||||||
using Foxchat.Identity.Database;
|
using Foxchat.Identity.Database;
|
||||||
using Foxchat.Identity.Utils;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace Foxchat.Identity.Middleware;
|
namespace Foxchat.Identity.Authorization;
|
||||||
|
|
||||||
public class ClientAuthorizationMiddleware(
|
public class AuthorizationMiddleware(
|
||||||
IdentityContext db,
|
IdentityContext db,
|
||||||
IClock clock
|
IClock clock
|
||||||
) : IMiddleware
|
) : IMiddleware
|
||||||
|
@ -22,10 +22,10 @@ public class ClientAuthorizationMiddleware(
|
||||||
}
|
}
|
||||||
|
|
||||||
var token = ctx.GetToken();
|
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.");
|
throw new ApiError.Unauthorized("This endpoint requires an authenticated user.");
|
||||||
if (attribute.Scopes.Length > 0 && attribute.Scopes.Except(token.Scopes.ExpandScopes()).Any())
|
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.ExpandScopes()));
|
throw new ApiError.Forbidden("This endpoint requires ungranted scopes.", attribute.Scopes.Except(token.Scopes));
|
||||||
|
|
||||||
await next(ctx);
|
await next(ctx);
|
||||||
}
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
using Foxchat.Core;
|
|
||||||
using Foxchat.Core.Models.Http;
|
using Foxchat.Core.Models.Http;
|
||||||
using Foxchat.Identity.Database;
|
using Foxchat.Identity.Database;
|
||||||
using Foxchat.Identity.Services;
|
using Foxchat.Identity.Services;
|
||||||
|
@ -8,16 +7,15 @@ namespace Foxchat.Identity.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/_fox/ident/node")]
|
[Route("/_fox/ident/node")]
|
||||||
public class NodeController(IdentityContext db, ChatInstanceResolverService chatInstanceResolverService)
|
public class NodeController(IdentityContext db, ChatInstanceResolverService chatInstanceResolverService) : ControllerBase
|
||||||
: ControllerBase
|
|
||||||
{
|
{
|
||||||
private const string SoftwareName = "Foxchat.NET.Identity";
|
public const string SOFTWARE_NAME = "Foxchat.NET.Identity";
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetNode()
|
public async Task<IActionResult> GetNode()
|
||||||
{
|
{
|
||||||
var instance = await db.GetInstanceAsync();
|
var instance = await db.GetInstanceAsync();
|
||||||
return Ok(new Hello.NodeInfo(new Hello.NodeSoftware(SoftwareName, BuildInfo.Version), instance.PublicKey));
|
return Ok(new Hello.NodeInfo(SOFTWARE_NAME, instance.PublicKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{domain}")]
|
[HttpGet("{domain}")]
|
||||||
|
@ -26,4 +24,4 @@ public class NodeController(IdentityContext db, ChatInstanceResolverService chat
|
||||||
var instance = await chatInstanceResolverService.ResolveChatInstanceAsync(domain);
|
var instance = await chatInstanceResolverService.ResolveChatInstanceAsync(domain);
|
||||||
return Ok(instance);
|
return Ok(instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,27 @@
|
||||||
using Foxchat.Core;
|
using Foxchat.Core;
|
||||||
using Foxchat.Core.Models.Http;
|
using Foxchat.Core.Models.Http;
|
||||||
using Foxchat.Identity.Middleware;
|
using Foxchat.Identity.Authorization;
|
||||||
using Foxchat.Identity.Database;
|
using Foxchat.Identity.Database;
|
||||||
using Foxchat.Identity.Database.Models;
|
using Foxchat.Identity.Database.Models;
|
||||||
using Foxchat.Identity.Utils;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Foxchat.Identity.Controllers.Oauth;
|
namespace Foxchat.Identity.Controllers.Oauth;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[ClientAuthenticate]
|
[Authenticate]
|
||||||
[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] AppsApi.CreateRequest req)
|
public async Task<IActionResult> CreateApplication([FromBody] Apps.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);
|
await db.AddAsync(app);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
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 AppsApi.CreateResponse(
|
return Ok(new Apps.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
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -30,9 +29,11 @@ public class AppsController(ILogger logger, IdentityContext db) : ControllerBase
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult GetSelfApp([FromQuery(Name = "with_secret")] bool withSecret)
|
public IActionResult GetSelfApp([FromQuery(Name = "with_secret")] bool withSecret)
|
||||||
{
|
{
|
||||||
var app = HttpContext.GetApplicationOrThrow();
|
var token = HttpContext.GetToken();
|
||||||
|
if (token is not { Account: null }) throw new ApiError.Forbidden("This endpoint requires a client token.");
|
||||||
|
var app = token.Application;
|
||||||
|
|
||||||
return Ok(new AppsApi.GetSelfResponse(
|
return Ok(new Apps.GetSelfResponse(
|
||||||
app.Id,
|
app.Id,
|
||||||
app.ClientId,
|
app.ClientId,
|
||||||
withSecret ? app.ClientSecret : null,
|
withSecret ? app.ClientSecret : null,
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
using Foxchat.Identity.Middleware;
|
|
||||||
using Foxchat.Identity.Database;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
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]
|
|
||||||
[Route("/_fox/ident/oauth/password")]
|
|
||||||
public class PasswordAuthController(ILogger logger, IdentityContext db, IClock clock) : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly PasswordHasher<Account> _passwordHasher = new();
|
|
||||||
|
|
||||||
[HttpPost("register")]
|
|
||||||
public async Task<IActionResult> Register([FromBody] RegisterRequest req)
|
|
||||||
{
|
|
||||||
var app = HttpContext.GetApplicationOrThrow();
|
|
||||||
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())
|
|
||||||
throw new ApiError.Forbidden("Cannot request token scopes that are not allowed for this token",
|
|
||||||
req.Scopes.Except(appScopes));
|
|
||||||
|
|
||||||
var acct = new Account
|
|
||||||
{
|
|
||||||
Username = req.Username,
|
|
||||||
Email = req.Email,
|
|
||||||
Role = Account.AccountRole.User
|
|
||||||
};
|
|
||||||
|
|
||||||
db.Add(acct);
|
|
||||||
var hashedPassword = await Task.Run(() => _passwordHasher.HashPassword(acct, req.Password));
|
|
||||||
acct.Password = hashedPassword;
|
|
||||||
// TODO: make token expiry configurable
|
|
||||||
var (tokenStr, token) = Token.Create(acct, app, req.Scopes, clock.GetCurrentInstant() + Duration.FromDays(365));
|
|
||||||
db.Add(token);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return Ok(new AuthResponse(acct.Id, acct.Username, acct.Email, tokenStr));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("login")]
|
|
||||||
public async Task<IActionResult> Login([FromBody] LoginRequest req)
|
|
||||||
{
|
|
||||||
var app = HttpContext.GetApplicationOrThrow();
|
|
||||||
var appToken = HttpContext.GetToken() ?? throw new UnreachableException();
|
|
||||||
var appScopes = appToken.Scopes.ExpandScopes();
|
|
||||||
|
|
||||||
if (req.Scopes.Except(appScopes).Any())
|
|
||||||
throw new ApiError.Forbidden("Cannot request token scopes that are not allowed for this token",
|
|
||||||
req.Scopes.Except(appScopes));
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
var pwResult = await Task.Run(() => _passwordHasher.VerifyHashedPassword(acct, acct.Password, req.Password));
|
|
||||||
if (pwResult == PasswordVerificationResult.Failed)
|
|
||||||
throw new ApiError.NotFound("No user with that email found, or password is incorrect");
|
|
||||||
if (pwResult == PasswordVerificationResult.SuccessRehashNeeded)
|
|
||||||
acct.Password = await Task.Run(() => _passwordHasher.HashPassword(acct, req.Password));
|
|
||||||
|
|
||||||
var (tokenStr, token) = Token.Create(acct, app, req.Scopes, clock.GetCurrentInstant() + Duration.FromDays(365));
|
|
||||||
db.Add(token);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return Ok(new AuthResponse(acct.Id, acct.Username, acct.Email, tokenStr));
|
|
||||||
}
|
|
||||||
|
|
||||||
public record RegisterRequest(string Username, string Password, string Email, string[] Scopes);
|
|
||||||
|
|
||||||
public record LoginRequest(string Email, string Password, string[] Scopes);
|
|
||||||
|
|
||||||
public record AuthResponse(Ulid Id, string Username, string Email, string Token);
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
using Foxchat.Core;
|
using Foxchat.Core;
|
||||||
using Foxchat.Identity.Database;
|
using Foxchat.Identity.Database;
|
||||||
using Foxchat.Identity.Database.Models;
|
using Foxchat.Identity.Database.Models;
|
||||||
using Foxchat.Identity.Utils;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
|
@ -15,20 +14,18 @@ public class TokenController(ILogger logger, IdentityContext db, IClock clock) :
|
||||||
public async Task<IActionResult> PostToken([FromBody] PostTokenRequest req)
|
public async Task<IActionResult> PostToken([FromBody] PostTokenRequest req)
|
||||||
{
|
{
|
||||||
var app = await db.GetApplicationAsync(req.ClientId, req.ClientSecret);
|
var app = await db.GetApplicationAsync(req.ClientId, req.ClientSecret);
|
||||||
var appScopes = app.Scopes.ExpandScopes();
|
|
||||||
|
|
||||||
var scopes = req.Scope.Split(' ');
|
var scopes = req.Scope.Split(' ');
|
||||||
if (scopes.Except(appScopes).Any())
|
if (app.Scopes.Except(scopes).Any())
|
||||||
{
|
{
|
||||||
throw new ApiError.Forbidden("Invalid or unauthorized scopes", scopes.Except(appScopes));
|
throw new ApiError.BadRequest("Invalid or unauthorized scopes");
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (req.GrantType)
|
switch (req.GrantType)
|
||||||
{
|
{
|
||||||
case OauthUtils.ClientCredentials:
|
case "client_credentials":
|
||||||
return await HandleClientCredentialsAsync(app, scopes);
|
return await HandleClientCredentialsAsync(app, scopes);
|
||||||
case OauthUtils.AuthorizationCode:
|
case "authorization_code":
|
||||||
// TODO
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ApiError.BadRequest("Unknown grant_type");
|
throw new ApiError.BadRequest("Unknown grant_type");
|
||||||
|
@ -41,9 +38,16 @@ public class TokenController(ILogger logger, IdentityContext db, IClock clock) :
|
||||||
{
|
{
|
||||||
// TODO: make this configurable
|
// TODO: make this configurable
|
||||||
var expiry = clock.GetCurrentInstant() + Duration.FromDays(365);
|
var expiry = clock.GetCurrentInstant() + Duration.FromDays(365);
|
||||||
var (token, tokenObj) = Token.Create(null, app, scopes, expiry);
|
var (token, hash) = Token.Generate();
|
||||||
|
var tokenObj = new Token
|
||||||
|
{
|
||||||
|
Hash = hash,
|
||||||
|
Scopes = scopes,
|
||||||
|
Expires = expiry,
|
||||||
|
ApplicationId = app.Id
|
||||||
|
};
|
||||||
|
|
||||||
db.Add(tokenObj);
|
await db.AddAsync(tokenObj);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
logger.Debug("Created token with scopes {Scopes} for application {ApplicationId}", scopes, app.Id);
|
logger.Debug("Created token with scopes {Scopes} for application {ApplicationId}", scopes, app.Id);
|
||||||
|
|
|
@ -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<IActionResult> CreateGuild([FromBody] GuildsApi.CreateGuildRequest req) =>
|
|
||||||
Proxy<Guilds.Guild>(HttpMethod.Post, req);
|
|
||||||
}
|
|
|
@ -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<IActionResult> Proxy<TResponse>(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<T> 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<TResponse>(method, server, path, acct.Id.ToString(), body);
|
|
||||||
return Ok(resp);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<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));
|
|
||||||
}
|
|
||||||
|
|
||||||
[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
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
using Foxchat.Core;
|
using Foxchat.Core;
|
||||||
using Foxchat.Core.Database;
|
using Foxchat.Core.Database;
|
||||||
using Foxchat.Core.Extensions;
|
|
||||||
using Foxchat.Identity.Database.Models;
|
using Foxchat.Identity.Database.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
@ -11,16 +10,15 @@ namespace Foxchat.Identity.Database;
|
||||||
public class IdentityContext : IDatabaseContext
|
public class IdentityContext : IDatabaseContext
|
||||||
{
|
{
|
||||||
private readonly NpgsqlDataSource _dataSource;
|
private readonly NpgsqlDataSource _dataSource;
|
||||||
private readonly ILoggerFactory? _loggerFactory;
|
|
||||||
|
|
||||||
public override DbSet<Instance> Instance { get; set; }
|
|
||||||
public DbSet<Account> Accounts { get; set; }
|
public DbSet<Account> Accounts { get; set; }
|
||||||
public DbSet<ChatInstance> ChatInstances { get; set; }
|
public DbSet<ChatInstance> ChatInstances { get; set; }
|
||||||
|
public override DbSet<Instance> Instance { get; set; }
|
||||||
public DbSet<Application> Applications { get; set; }
|
public DbSet<Application> Applications { get; set; }
|
||||||
public DbSet<Token> Tokens { get; set; }
|
public DbSet<Token> Tokens { get; set; }
|
||||||
public DbSet<GuildAccount> GuildAccounts { get; set; }
|
public DbSet<GuildAccount> GuildAccounts { get; set; }
|
||||||
|
|
||||||
public IdentityContext(InstanceConfig config, ILoggerFactory? loggerFactory)
|
public IdentityContext(InstanceConfig config)
|
||||||
{
|
{
|
||||||
var connString = new NpgsqlConnectionStringBuilder(config.Database.Url)
|
var connString = new NpgsqlConnectionStringBuilder(config.Database.Url)
|
||||||
{
|
{
|
||||||
|
@ -31,14 +29,12 @@ public class IdentityContext : IDatabaseContext
|
||||||
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString);
|
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString);
|
||||||
dataSourceBuilder.UseNodaTime();
|
dataSourceBuilder.UseNodaTime();
|
||||||
_dataSource = dataSourceBuilder.Build();
|
_dataSource = dataSourceBuilder.Build();
|
||||||
_loggerFactory = loggerFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
=> optionsBuilder
|
=> optionsBuilder
|
||||||
.UseNpgsql(_dataSource, o => o.UseNodaTime())
|
.UseNpgsql(_dataSource, o => o.UseNodaTime())
|
||||||
.UseSnakeCaseNamingConvention()
|
.UseSnakeCaseNamingConvention();
|
||||||
.UseLoggerFactory(_loggerFactory);
|
|
||||||
|
|
||||||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
|
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
|
||||||
{
|
{
|
||||||
|
@ -59,7 +55,6 @@ public class IdentityContext : IDatabaseContext
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReSharper disable once UnusedType.Global
|
|
||||||
public class DesignTimeIdentityContextFactory : IDesignTimeDbContextFactory<IdentityContext>
|
public class DesignTimeIdentityContextFactory : IDesignTimeDbContextFactory<IdentityContext>
|
||||||
{
|
{
|
||||||
public IdentityContext CreateDbContext(string[] args)
|
public IdentityContext CreateDbContext(string[] args)
|
||||||
|
@ -71,6 +66,6 @@ public class DesignTimeIdentityContextFactory : IDesignTimeDbContextFactory<Iden
|
||||||
// Get the configuration as our config class
|
// Get the configuration as our config class
|
||||||
.Get<InstanceConfig>() ?? new();
|
.Get<InstanceConfig>() ?? new();
|
||||||
|
|
||||||
return new IdentityContext(config, null);
|
return new IdentityContext(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,17 +24,4 @@ public class Token : BaseModel
|
||||||
|
|
||||||
return (token, hash);
|
return (token, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (string, Token) Create(Account? account, Application application, string[] scopes, Instant expires)
|
|
||||||
{
|
|
||||||
var (token, hash) = Generate();
|
|
||||||
return (token, new()
|
|
||||||
{
|
|
||||||
Hash = hash,
|
|
||||||
Scopes = scopes,
|
|
||||||
Expires = expires,
|
|
||||||
Account = account,
|
|
||||||
Application = application,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using Foxchat.Core.Middleware;
|
using Foxchat.Identity.Authorization;
|
||||||
using Foxchat.Identity.Middleware;
|
|
||||||
|
|
||||||
namespace Foxchat.Identity.Extensions;
|
namespace Foxchat.Identity.Extensions;
|
||||||
|
|
||||||
|
@ -8,16 +7,14 @@ public static class WebApplicationExtensions
|
||||||
public static IServiceCollection AddCustomMiddleware(this IServiceCollection services)
|
public static IServiceCollection AddCustomMiddleware(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
return services
|
return services
|
||||||
.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<AuthenticationMiddleware>()
|
||||||
.UseMiddleware<ClientAuthenticationMiddleware>()
|
.UseMiddleware<AuthorizationMiddleware>();
|
||||||
.UseMiddleware<ClientAuthorizationMiddleware>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,20 @@
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Foxchat.Core;
|
using Foxchat.Core;
|
||||||
using Foxchat.Core.Extensions;
|
|
||||||
using Foxchat.Identity;
|
using Foxchat.Identity;
|
||||||
using Foxchat.Identity.Database;
|
using Foxchat.Identity.Database;
|
||||||
using Foxchat.Identity.Services;
|
using Foxchat.Identity.Services;
|
||||||
using Foxchat.Identity.Extensions;
|
using Foxchat.Identity.Extensions;
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
var config = builder.AddConfiguration<InstanceConfig>("identity.ini");
|
var config = builder.AddConfiguration<InstanceConfig>("identity.ini");
|
||||||
|
|
||||||
builder.AddSerilog();
|
builder.AddSerilog(config.LogEventLevel);
|
||||||
|
|
||||||
await BuildInfo.ReadBuildInfo();
|
await BuildInfo.ReadBuildInfo();
|
||||||
Log.Information("Starting Foxchat.Identity {Version} ({Hash})", BuildInfo.Version, BuildInfo.Hash);
|
Log.Information("Starting Foxchat.Identity {Version} ({Hash})", BuildInfo.Version, BuildInfo.Hash);
|
||||||
|
|
||||||
// Set the default converter to snake case as we use it in a couple places.
|
|
||||||
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
ContractResolver = new DefaultContractResolver
|
|
||||||
{
|
|
||||||
NamingStrategy = new SnakeCaseNamingStrategy()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddControllers()
|
.AddControllers()
|
||||||
.AddNewtonsoftJson(options =>
|
.AddNewtonsoftJson(options =>
|
||||||
|
@ -52,7 +41,7 @@ app.UseCustomMiddleware();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
await using (var context = scope.ServiceProvider.GetRequiredService<IdentityContext>())
|
using (var context = scope.ServiceProvider.GetRequiredService<IdentityContext>())
|
||||||
{
|
{
|
||||||
Log.Information("Initializing instance keypair...");
|
Log.Information("Initializing instance keypair...");
|
||||||
if (await context.InitializeInstanceAsync())
|
if (await context.InitializeInstanceAsync())
|
||||||
|
|
|
@ -31,7 +31,7 @@ public class ChatInstanceResolverService(ILogger logger, RequestSigningService r
|
||||||
PublicKey = resp.PublicKey,
|
PublicKey = resp.PublicKey,
|
||||||
Status = ChatInstance.InstanceStatus.Active,
|
Status = ChatInstance.InstanceStatus.Active,
|
||||||
};
|
};
|
||||||
db.Add(instance);
|
await db.AddAsync(instance);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
|
|
@ -1,15 +1,8 @@
|
||||||
using Foxchat.Core;
|
|
||||||
using Foxchat.Identity.Middleware;
|
|
||||||
using Foxchat.Identity.Database.Models;
|
|
||||||
|
|
||||||
namespace Foxchat.Identity.Utils;
|
namespace Foxchat.Identity.Utils;
|
||||||
|
|
||||||
public static class OauthUtils
|
public static class OauthUtils
|
||||||
{
|
{
|
||||||
public const string ClientCredentials = "client_credentials";
|
public static readonly string[] Scopes = ["identify", "chat_client"];
|
||||||
public const string AuthorizationCode = "authorization_code";
|
|
||||||
|
|
||||||
public static readonly string[] Scopes = ["identify", "email", "guilds", "chat_client"];
|
|
||||||
|
|
||||||
private static readonly string[] ForbiddenSchemes = ["javascript", "file", "data", "mailto", "tel"];
|
private static readonly string[] ForbiddenSchemes = ["javascript", "file", "data", "mailto", "tel"];
|
||||||
private const string OobUri = "urn:ietf:wg:oauth:2.0:oob";
|
private const string OobUri = "urn:ietf:wg:oauth:2.0:oob";
|
||||||
|
@ -27,8 +20,4 @@ public static class OauthUtils
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public static string[] ExpandScopes(this string[] scopes) => scopes.Contains("chat_client")
|
|
||||||
? Scopes
|
|
||||||
: scopes;
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,6 +2,11 @@ Host = localhost
|
||||||
Port = 7611
|
Port = 7611
|
||||||
Domain = id.fox.localhost
|
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]
|
[Database]
|
||||||
; The database URL in ADO.NET format.
|
; The database URL in ADO.NET format.
|
||||||
Url = "Host=localhost;Database=foxchat_cs_ident;Username=foxchat;Password=password"
|
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
|
Timeout = 5
|
||||||
; The maximum number of open connections. Defaults to 50.
|
; The maximum number of open connections. Defaults to 50.
|
||||||
MaxPoolSize = 500
|
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
|
|
||||||
|
|
Loading…
Reference in a new issue