chat: add database types and auth middleware
This commit is contained in:
parent
656eec81d8
commit
6f6e19bbb5
24 changed files with 1165 additions and 15 deletions
13
.idea/.idea.Foxchat/.idea/.gitignore
vendored
Normal file
13
.idea/.idea.Foxchat/.idea/.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# 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
.idea/.idea.Foxchat/.idea/.name
Normal file
1
.idea/.idea.Foxchat/.idea/.name
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Foxchat
|
4
.idea/.idea.Foxchat/.idea/encodings.xml
Normal file
4
.idea/.idea.Foxchat/.idea/encodings.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||||
|
</project>
|
8
.idea/.idea.Foxchat/.idea/indexLayout.xml
Normal file
8
.idea/.idea.Foxchat/.idea/indexLayout.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="UserContentModel">
|
||||||
|
<attachedFolders />
|
||||||
|
<explicitIncludes />
|
||||||
|
<explicitExcludes />
|
||||||
|
</component>
|
||||||
|
</project>
|
7
.idea/.idea.Foxchat/.idea/sqldialects.xml
Normal file
7
.idea/.idea.Foxchat/.idea/sqldialects.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?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>
|
6
.idea/.idea.Foxchat/.idea/vcs.xml
Normal file
6
.idea/.idea.Foxchat/.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
6
Foxchat.Chat/Database/BaseModel.cs
Normal file
6
Foxchat.Chat/Database/BaseModel.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Foxchat.Chat.Database;
|
||||||
|
|
||||||
|
public abstract class BaseModel
|
||||||
|
{
|
||||||
|
public Ulid Id { get; init; } = Ulid.NewUlid();
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Foxchat.Chat.Database.Models;
|
||||||
using Foxchat.Core;
|
using Foxchat.Core;
|
||||||
using Foxchat.Core.Database;
|
using Foxchat.Core.Database;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -11,6 +12,11 @@ public class ChatContext : IDatabaseContext
|
||||||
private readonly NpgsqlDataSource _dataSource;
|
private readonly NpgsqlDataSource _dataSource;
|
||||||
|
|
||||||
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)
|
public ChatContext(InstanceConfig config)
|
||||||
{
|
{
|
||||||
|
@ -38,16 +44,31 @@ 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("identity.ini")
|
.AddConfiguration("chat.ini")
|
||||||
.Build()
|
.Build()
|
||||||
// Get the configuration as our config class
|
// Get the configuration as our config class
|
||||||
.Get<InstanceConfig>() ?? new();
|
.Get<InstanceConfig>() ?? new();
|
||||||
|
|
9
Foxchat.Chat/Database/Models/Channel.cs
Normal file
9
Foxchat.Chat/Database/Models/Channel.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
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; }
|
||||||
|
}
|
11
Foxchat.Chat/Database/Models/Guild.cs
Normal file
11
Foxchat.Chat/Database/Models/Guild.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
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; } = [];
|
||||||
|
}
|
17
Foxchat.Chat/Database/Models/IdentityInstance.cs
Normal file
17
Foxchat.Chat/Database/Models/IdentityInstance.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
15
Foxchat.Chat/Database/Models/Message.cs
Normal file
15
Foxchat.Chat/Database/Models/Message.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
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; }
|
||||||
|
}
|
14
Foxchat.Chat/Database/Models/User.cs
Normal file
14
Foxchat.Chat/Database/Models/User.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
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 List<Guild> Guilds { get; } = [];
|
||||||
|
public List<Guild> OwnedGuilds { get; } = [];
|
||||||
|
}
|
18
Foxchat.Chat/Extensions/WebApplicationExtensions.cs
Normal file
18
Foxchat.Chat/Extensions/WebApplicationExtensions.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using Foxchat.Chat.Middleware;
|
||||||
|
|
||||||
|
namespace Foxchat.Chat.Extensions;
|
||||||
|
|
||||||
|
public static class WebApplicationExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddCustomMiddleware(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
return services
|
||||||
|
.AddScoped<AuthenticationMiddleware>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
return app
|
||||||
|
.UseMiddleware<AuthenticationMiddleware>();
|
||||||
|
}
|
||||||
|
}
|
106
Foxchat.Chat/Middleware/AuthenticationMiddleware.cs
Normal file
106
Foxchat.Chat/Middleware/AuthenticationMiddleware.cs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
using Foxchat.Chat.Database;
|
||||||
|
using Foxchat.Chat.Database.Models;
|
||||||
|
using Foxchat.Core;
|
||||||
|
using Foxchat.Core.Federation;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Foxchat.Chat.Middleware;
|
||||||
|
|
||||||
|
public class AuthenticationMiddleware(ILogger logger, ChatContext db, RequestSigningService requestSigningService)
|
||||||
|
: IMiddleware
|
||||||
|
{
|
||||||
|
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
|
||||||
|
{
|
||||||
|
var endpoint = ctx.GetEndpoint();
|
||||||
|
// Endpoints require server authentication by default, unless they have the [Unauthenticated] attribute.
|
||||||
|
var metadata = endpoint?.Metadata.GetMetadata<UnauthenticatedAttribute>();
|
||||||
|
if (metadata != null)
|
||||||
|
{
|
||||||
|
await next(ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ExtractRequestData(ctx, 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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ExtractRequestData(HttpContext ctx, out string signature, out string domain, out SignatureData data)
|
||||||
|
{
|
||||||
|
signature = string.Empty;
|
||||||
|
domain = string.Empty;
|
||||||
|
data = SignatureData.Empty;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||||
|
public class UnauthenticatedAttribute : 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!;
|
||||||
|
}
|
||||||
|
}
|
325
Foxchat.Chat/Migrations/20240521132416_Init.Designer.cs
generated
Normal file
325
Foxchat.Chat/Migrations/20240521132416_Init.Designer.cs
generated
Normal file
|
@ -0,0 +1,325 @@
|
||||||
|
// <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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
228
Foxchat.Chat/Migrations/20240521132416_Init.cs
Normal file
228
Foxchat.Chat/Migrations/20240521132416_Init.cs
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
322
Foxchat.Chat/Migrations/ChatContextModelSnapshot.cs
Normal file
322
Foxchat.Chat/Migrations/ChatContextModelSnapshot.cs
Normal file
|
@ -0,0 +1,322 @@
|
||||||
|
// <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<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,6 +3,7 @@ using Serilog;
|
||||||
using Foxchat.Core;
|
using Foxchat.Core;
|
||||||
using Foxchat.Chat;
|
using Foxchat.Chat;
|
||||||
using Foxchat.Chat.Database;
|
using Foxchat.Chat.Database;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
@ -13,6 +14,15 @@ 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 =>
|
||||||
|
@ -38,7 +48,7 @@ app.UseAuthorization();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
using (var context = scope.ServiceProvider.GetRequiredService<ChatContext>())
|
await 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())
|
||||||
|
|
|
@ -31,23 +31,22 @@ public partial class RequestSigningService(ILogger logger, IClock clock, IDataba
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool VerifySignature(
|
public bool VerifySignature(
|
||||||
string publicKey, string encodedSignature, string dateHeader, string host, string requestPath, int? contentLength, string? userId)
|
string publicKey, string encodedSignature, SignatureData data)
|
||||||
{
|
{
|
||||||
var rsa = RSA.Create();
|
var rsa = RSA.Create();
|
||||||
rsa.ImportFromPem(publicKey);
|
rsa.ImportFromPem(publicKey);
|
||||||
|
|
||||||
var now = _clock.GetCurrentInstant();
|
var now = _clock.GetCurrentInstant();
|
||||||
var time = ParseTime(dateHeader);
|
if ((now + Duration.FromMinutes(1)) < data.Time)
|
||||||
if ((now + Duration.FromMinutes(1)) < time)
|
|
||||||
{
|
{
|
||||||
throw new ApiError.IncomingFederationError("Request was made in the future");
|
throw new ApiError.IncomingFederationError("Request was made in the future");
|
||||||
}
|
}
|
||||||
else if ((now - Duration.FromMinutes(1)) > time)
|
else if ((now - Duration.FromMinutes(1)) > data.Time)
|
||||||
{
|
{
|
||||||
throw new ApiError.IncomingFederationError("Request was made too long ago");
|
throw new ApiError.IncomingFederationError("Request was made too long ago");
|
||||||
}
|
}
|
||||||
|
|
||||||
var plaintext = GeneratePlaintext(new SignatureData(time, host, requestPath, contentLength, userId));
|
var plaintext = GeneratePlaintext(data);
|
||||||
var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
|
var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
|
||||||
var hash = SHA256.HashData(plaintextBytes);
|
var hash = SHA256.HashData(plaintextBytes);
|
||||||
|
|
||||||
|
@ -73,5 +72,5 @@ public partial class RequestSigningService(ILogger logger, IClock clock, IDataba
|
||||||
|
|
||||||
private static readonly InstantPattern _pattern = InstantPattern.Create("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.GetCultureInfo("en-US"));
|
private static readonly InstantPattern _pattern = InstantPattern.Create("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.GetCultureInfo("en-US"));
|
||||||
private static string FormatTime(Instant time) => _pattern.Format(time);
|
private static string FormatTime(Instant time) => _pattern.Format(time);
|
||||||
private static Instant ParseTime(string header) => _pattern.Parse(header).GetValueOrThrow();
|
public static Instant ParseTime(string header) => _pattern.Parse(header).GetValueOrThrow();
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,4 +8,13 @@ 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
|
||||||
);
|
);
|
||||||
|
}
|
|
@ -11,9 +11,9 @@ public class IdentityContext : IDatabaseContext
|
||||||
{
|
{
|
||||||
private readonly NpgsqlDataSource _dataSource;
|
private readonly NpgsqlDataSource _dataSource;
|
||||||
|
|
||||||
|
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; }
|
||||||
|
@ -55,6 +55,7 @@ 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)
|
||||||
|
|
|
@ -51,7 +51,7 @@ app.UseCustomMiddleware();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
using (var context = scope.ServiceProvider.GetRequiredService<IdentityContext>())
|
await 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())
|
||||||
|
|
Loading…
Reference in a new issue