add a bunch of authentication stuff
This commit is contained in:
		
							parent
							
								
									996e59f49a
								
							
						
					
					
						commit
						aca83fa1ef
					
				
					 22 changed files with 681 additions and 28 deletions
				
			
		|  | @ -2,27 +2,32 @@ using Foxchat.Core; | |||
| using Foxchat.Core.Database; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Design; | ||||
| using Npgsql; | ||||
| 
 | ||||
| namespace Foxchat.Chat.Database; | ||||
| 
 | ||||
| public class ChatContext : IDatabaseContext | ||||
| { | ||||
|     private readonly string _connString; | ||||
|     private readonly NpgsqlDataSource _dataSource; | ||||
| 
 | ||||
|     public override DbSet<Instance> Instance { get; set; } | ||||
| 
 | ||||
|     public ChatContext(InstanceConfig config) | ||||
|     { | ||||
|         _connString = new Npgsql.NpgsqlConnectionStringBuilder(config.Database.Url) | ||||
|         var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) | ||||
|         { | ||||
|             Timeout = config.Database.Timeout ?? 5, | ||||
|             MaxPoolSize = config.Database.MaxPoolSize ?? 50, | ||||
|         }.ConnectionString; | ||||
| 
 | ||||
|         var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); | ||||
|         dataSourceBuilder.UseNodaTime(); | ||||
|         _dataSource = dataSourceBuilder.Build(); | ||||
|     } | ||||
| 
 | ||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
|             => optionsBuilder | ||||
|                 .UseNpgsql(_connString) | ||||
|                 .UseNpgsql(_dataSource, o => o.UseNodaTime()) | ||||
|                 .UseSnakeCaseNamingConvention(); | ||||
| 
 | ||||
|     protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ | |||
|       <PrivateAssets>all</PrivateAssets> | ||||
|     </PackageReference> | ||||
|     <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" /> | ||||
|     <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="8.0.4" /> | ||||
|     <PackageReference Include="Serilog" Version="3.1.1" /> | ||||
|     <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /> | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ var builder = WebApplication.CreateBuilder(args); | |||
| 
 | ||||
| var config = builder.AddConfiguration<InstanceConfig>("chat.ini"); | ||||
| 
 | ||||
| builder.Services.AddSerilog(config.LogEventLevel); | ||||
| builder.AddSerilog(config.LogEventLevel); | ||||
| 
 | ||||
| await BuildInfo.ReadBuildInfo(); | ||||
| Log.Information("Starting Foxchat.Chat {Version} ({Hash})", BuildInfo.Version, BuildInfo.Hash); | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ public class CoreConfig | |||
|     public string Address => $"{(Secure ? "https" : "http")}://{Host}:{Port}"; | ||||
| 
 | ||||
|     public LogEventLevel LogEventLevel { get; set; } = LogEventLevel.Debug; | ||||
|     public string? SeqLogUrl { get; set; } | ||||
| 
 | ||||
|     public DatabaseConfig Database { get; set; } = new(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -30,12 +30,12 @@ public partial class RequestSigningService | |||
|         if (!resp.IsSuccessStatusCode) | ||||
|         { | ||||
|             var error = await resp.Content.ReadAsStringAsync(); | ||||
|             throw new FoxchatError.OutgoingFederationError($"Request to {domain}/{requestPath} returned an error", DeserializeObject<ApiError>(error)); | ||||
|             throw new FoxchatError.OutgoingFederationError($"Request to {domain}{requestPath} returned an error", DeserializeObject<ApiError>(error)); | ||||
|         } | ||||
| 
 | ||||
|         var bodyString = await resp.Content.ReadAsStringAsync(); | ||||
|         return DeserializeObject<T>(bodyString) | ||||
|             ?? throw new FoxchatError.OutgoingFederationError($"Request to {domain}/{requestPath} returned invalid response body"); | ||||
|             ?? throw new FoxchatError.OutgoingFederationError($"Request to {domain}{requestPath} returned invalid response body"); | ||||
|     } | ||||
| 
 | ||||
|     private HttpRequestMessage BuildHttpRequest(HttpMethod method, string domain, string requestPath, string? userId = null, object? bodyData = null) | ||||
|  | @ -55,9 +55,7 @@ public partial class RequestSigningService | |||
|         if (userId != null) | ||||
|             request.Headers.Add(USER_HEADER, userId); | ||||
|         if (body != null) | ||||
|         { | ||||
|             request.Content = new StringContent(body, new MediaTypeHeaderValue("application/json", "utf-8")); | ||||
|         } | ||||
| 
 | ||||
|         return request; | ||||
|     } | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ using System.Globalization; | |||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using Foxchat.Core.Database; | ||||
| using Microsoft.AspNetCore.WebUtilities; | ||||
| using Foxchat.Core.Utils; | ||||
| using NodaTime; | ||||
| using NodaTime.Text; | ||||
| using Serilog; | ||||
|  | @ -28,7 +28,7 @@ public partial class RequestSigningService(ILogger logger, IClock clock, IDataba | |||
|         var signature = formatter.CreateSignature(hash); | ||||
| 
 | ||||
|         _logger.Debug("Generated signature for {Host} {RequestPath}", data.Host, data.RequestPath); | ||||
|         return WebEncoders.Base64UrlEncode(signature); | ||||
|         return Convert.ToBase64String(signature); | ||||
|     } | ||||
| 
 | ||||
|     public bool VerifySignature( | ||||
|  | @ -51,7 +51,11 @@ public partial class RequestSigningService(ILogger logger, IClock clock, IDataba | |||
|         var plaintext = GeneratePlaintext(new SignatureData(time, host, requestPath, contentLength, userId)); | ||||
|         var plaintextBytes = Encoding.UTF8.GetBytes(plaintext); | ||||
|         var hash = SHA256.HashData(plaintextBytes); | ||||
|         var signature = WebEncoders.Base64UrlDecode(encodedSignature); | ||||
| 
 | ||||
|         if (!CryptoUtils.TryFromBase64String(encodedSignature, out var signature)) | ||||
|         { | ||||
|             throw new FoxchatError.IncomingFederationError("Invalid base64 signature"); | ||||
|         } | ||||
| 
 | ||||
|         var deformatter = new RSAPKCS1SignatureDeformatter(rsa); | ||||
|         deformatter.SetHashAlgorithm(nameof(SHA256)); | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ | |||
|     <PackageReference Include="Serilog" Version="3.1.1" /> | ||||
|     <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation"> | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ using Microsoft.Extensions.DependencyInjection; | |||
| using NodaTime; | ||||
| using Serilog; | ||||
| using Serilog.Events; | ||||
| using Serilog.Sinks.SystemConsole.Themes; | ||||
| 
 | ||||
| namespace Foxchat.Core; | ||||
| 
 | ||||
|  | @ -15,8 +14,10 @@ public static class ServiceCollectionExtensions | |||
|     /// <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. | ||||
|     /// </summary> | ||||
|     public static IServiceCollection AddSerilog(this IServiceCollection services, LogEventLevel level) | ||||
|     public static void AddSerilog(this WebApplicationBuilder builder, LogEventLevel level) | ||||
|     { | ||||
|         var config = builder.Configuration.Get<CoreConfig>() ?? new(); | ||||
| 
 | ||||
|         var logCfg = new LoggerConfiguration() | ||||
|             .Enrich.FromLogContext() | ||||
|             .MinimumLevel.Is(level) | ||||
|  | @ -26,12 +27,15 @@ public static class ServiceCollectionExtensions | |||
|             .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning) | ||||
|             .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) | ||||
|             .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) | ||||
|             .WriteTo.Console(theme: AnsiConsoleTheme.Code); | ||||
|             .WriteTo.Console(); | ||||
| 
 | ||||
|         if (config.SeqLogUrl != null) | ||||
|             logCfg.WriteTo.Seq(config.SeqLogUrl, restrictedToMinimumLevel: LogEventLevel.Verbose); | ||||
| 
 | ||||
|         Log.Logger = logCfg.CreateLogger(); | ||||
| 
 | ||||
|         // AddSerilog doesn't seem to add an ILogger to the service collection, so add that manually. | ||||
|         return services.AddSerilog().AddSingleton(Log.Logger); | ||||
|         builder.Services.AddSerilog().AddSingleton(Log.Logger); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|  |  | |||
							
								
								
									
										23
									
								
								Foxchat.Core/Utils/ConvertUtils.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								Foxchat.Core/Utils/ConvertUtils.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| using System.Security.Cryptography; | ||||
| 
 | ||||
| namespace Foxchat.Core.Utils; | ||||
| 
 | ||||
| public static class CryptoUtils | ||||
| { | ||||
|     public static bool TryFromBase64String(string b64, out byte[] bytes) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             bytes = Convert.FromBase64String(b64); | ||||
|             return true; | ||||
|         } | ||||
|         catch | ||||
|         { | ||||
|             bytes = []; | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static string RandomToken(int bytes = 48) => | ||||
|         Convert.ToBase64String(RandomNumberGenerator.GetBytes(bytes)).Trim('='); | ||||
| } | ||||
|  | @ -1,3 +1,14 @@ | |||
| using System.Security.Cryptography; | ||||
| using System.Text.Encodings.Web; | ||||
| using Foxchat.Core; | ||||
| using Foxchat.Core.Utils; | ||||
| using Foxchat.Identity.Database; | ||||
| using Microsoft.AspNetCore.Authentication; | ||||
| using Microsoft.AspNetCore.Authentication.BearerToken; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Options; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxchat.Identity.Authorization; | ||||
| 
 | ||||
| public static class AuthenticationHandlerExtensions | ||||
|  | @ -6,4 +17,34 @@ public static class AuthenticationHandlerExtensions | |||
|     { | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| } | ||||
| 
 | ||||
| public class FoxchatAuthenticationHandler( | ||||
|     IOptionsMonitor<BearerTokenOptions> options, | ||||
|     ILoggerFactory logger, | ||||
|     UrlEncoder encoder, | ||||
|     IdentityContext context, | ||||
|     IClock clock | ||||
| ) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder) | ||||
| { | ||||
|     protected override async Task<AuthenticateResult> HandleAuthenticateAsync() | ||||
|     { | ||||
|         var header = Request.Headers.Authorization.ToString(); | ||||
|         if (!header.StartsWith("bearer ", StringComparison.InvariantCultureIgnoreCase)) | ||||
|             return AuthenticateResult.NoResult(); | ||||
|         var token = header[7..]; | ||||
| 
 | ||||
|         if (!CryptoUtils.TryFromBase64String(token, out var rawToken)) | ||||
|             return AuthenticateResult.Fail(new FoxchatError.BadRequest("Invalid token format")); | ||||
| 
 | ||||
|         var hash = SHA512.HashData(rawToken); | ||||
|         var oauthToken = await context.Tokens | ||||
|             .Include(t => t.Account) | ||||
|             .Include(t => t.Application) | ||||
|             .FirstOrDefaultAsync(t => t.Hash == hash && t.Expires > clock.GetCurrentInstant()); | ||||
|         if (oauthToken == null) | ||||
|             return AuthenticateResult.NoResult(); | ||||
| 
 | ||||
|         throw new NotImplementedException(); | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										6
									
								
								Foxchat.Identity/Database/BaseModel.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								Foxchat.Identity/Database/BaseModel.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| namespace Foxchat.Identity.Database; | ||||
| 
 | ||||
| public abstract class BaseModel | ||||
| { | ||||
|     public Ulid Id { get; init; } = Ulid.NewUlid(); | ||||
| } | ||||
|  | @ -3,31 +3,37 @@ using Foxchat.Core.Database; | |||
| using Foxchat.Identity.Database.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Design; | ||||
| using Npgsql; | ||||
| 
 | ||||
| namespace Foxchat.Identity.Database; | ||||
| 
 | ||||
| public class IdentityContext : IDatabaseContext | ||||
| { | ||||
|     private readonly string _connString; | ||||
|     private readonly NpgsqlDataSource _dataSource; | ||||
| 
 | ||||
|     public DbSet<Account> Accounts { get; set; } | ||||
|     public DbSet<ChatInstance> ChatInstances { get; set; } | ||||
|     public override DbSet<Instance> Instance { get; set; } | ||||
|     public DbSet<Application> Applications { get; set; } | ||||
|     public DbSet<Token> Tokens { get; set; } | ||||
|     public DbSet<GuildAccount> GuildAccounts { get; set; } | ||||
| 
 | ||||
|     public IdentityContext(InstanceConfig config) | ||||
|     { | ||||
|         _connString = new Npgsql.NpgsqlConnectionStringBuilder(config.Database.Url) | ||||
|         var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) | ||||
|         { | ||||
|             Timeout = config.Database.Timeout ?? 5, | ||||
|             MaxPoolSize = config.Database.MaxPoolSize ?? 50, | ||||
|         }.ConnectionString; | ||||
| 
 | ||||
|         var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); | ||||
|         dataSourceBuilder.UseNodaTime(); | ||||
|         _dataSource = dataSourceBuilder.Build(); | ||||
|     } | ||||
| 
 | ||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
|             => optionsBuilder | ||||
|                 .UseNpgsql(_connString) | ||||
|                 .UseNpgsql(_dataSource, o => o.UseNodaTime()) | ||||
|                 .UseSnakeCaseNamingConvention(); | ||||
| 
 | ||||
|     protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) | ||||
|  | @ -44,6 +50,8 @@ public class IdentityContext : IDatabaseContext | |||
|         modelBuilder.Entity<ChatInstance>().HasIndex(i => i.Domain).IsUnique(); | ||||
| 
 | ||||
|         modelBuilder.Entity<GuildAccount>().HasKey(g => new { g.ChatInstanceId, g.GuildId, g.AccountId }); | ||||
| 
 | ||||
|         modelBuilder.Entity<Application>().HasIndex(a => a.ClientId).IsUnique(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,8 +1,7 @@ | |||
| namespace Foxchat.Identity.Database.Models; | ||||
| 
 | ||||
| public class Account | ||||
| public class Account : BaseModel | ||||
| { | ||||
|     public Ulid Id { get; init; } = Ulid.NewUlid(); | ||||
|     public string Username { get; set; } = null!; | ||||
|     public string Email { get; set; } = null!; | ||||
|     public string Password { get; set; } = null!; | ||||
|  |  | |||
							
								
								
									
										46
									
								
								Foxchat.Identity/Database/Models/Application.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								Foxchat.Identity/Database/Models/Application.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | |||
| using System.Security.Cryptography; | ||||
| using Microsoft.AspNetCore.WebUtilities; | ||||
| 
 | ||||
| namespace Foxchat.Identity.Database.Models; | ||||
| 
 | ||||
| public class Application : BaseModel | ||||
| { | ||||
|     public required string ClientId { get; init; } | ||||
|     public required string ClientSecret { get; init; } | ||||
|     public required string Name { get; init; } | ||||
|     public required string[] Scopes { get; init; } | ||||
| 
 | ||||
|     public static Application Create(string name, string[] scopes) | ||||
|     { | ||||
|         var clientId = RandomNumberGenerator.GetHexString(16, true); | ||||
|         var clientSecretBytes = RandomNumberGenerator.GetBytes(48); | ||||
|         var clientSecret = WebEncoders.Base64UrlEncode(clientSecretBytes); | ||||
| 
 | ||||
|         if (!scopes.All(s => Scope.ValidScopes.Contains(s))) | ||||
|         { | ||||
|             throw new ArgumentException("Invalid scopes passed to Application.Create", nameof(scopes)); | ||||
|         } | ||||
| 
 | ||||
|         return new Application | ||||
|         { | ||||
|             ClientId = clientId, | ||||
|             ClientSecret = clientSecret, | ||||
|             Name = name, | ||||
|             Scopes = scopes, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public static class Scope | ||||
| { | ||||
|     /// <summary> | ||||
|     /// OAuth scope for identifying a user and nothing else. | ||||
|     /// </summary> | ||||
|     public const string Identity = "identity"; | ||||
|     /// <summary> | ||||
|     /// OAuth scope for a full chat client. This grants *full access* to an account. | ||||
|     /// </summary> | ||||
|     public const string ChatClient = "chat_client"; | ||||
| 
 | ||||
|     public static readonly string[] ValidScopes = [Identity, ChatClient]; | ||||
| } | ||||
|  | @ -1,8 +1,7 @@ | |||
| namespace Foxchat.Identity.Database.Models; | ||||
| 
 | ||||
| public class ChatInstance | ||||
| public class ChatInstance : BaseModel | ||||
| { | ||||
|     public Ulid Id { get; init; } = Ulid.NewUlid(); | ||||
|     public string Domain { get; init; } = null!; | ||||
|     public string BaseUrl { get; set; } = null!; | ||||
|     public string PublicKey { get; set; } = null!; | ||||
|  |  | |||
|  | @ -1,8 +1,26 @@ | |||
| using System.Security.Cryptography; | ||||
| using Foxchat.Core.Utils; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxchat.Identity.Database.Models; | ||||
| 
 | ||||
| public class Token | ||||
| public class Token : BaseModel | ||||
| { | ||||
|     public Ulid Id { get; init; } = Ulid.NewUlid(); | ||||
|     public byte[] Hash { get; set; } = null!; | ||||
|     public string[] Scopes { get; set; } = []; | ||||
|     public Instant Expires { get; set; } | ||||
| 
 | ||||
|     public Ulid AccountId { get; set; } | ||||
|     public Account Account { get; set; } = null!; | ||||
| } | ||||
| 
 | ||||
|     public Ulid ApplicationId { get; set; } | ||||
|     public Application Application { get; set; } = null!; | ||||
| 
 | ||||
|     public static (string, byte[]) Generate() | ||||
|     { | ||||
|         var token = CryptoUtils.RandomToken(48); | ||||
|         var hash = SHA512.HashData(Convert.FromBase64String(token)); | ||||
| 
 | ||||
|         return (token, hash); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -16,7 +16,8 @@ | |||
|       <PrivateAssets>all</PrivateAssets> | ||||
|     </PackageReference> | ||||
|     <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | ||||
|     <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.2" /> | ||||
|     <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" /> | ||||
|     <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="8.0.4" /> | ||||
|     <PackageReference Include="Serilog" Version="3.1.1" /> | ||||
|     <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /> | ||||
|  |  | |||
							
								
								
									
										320
									
								
								Foxchat.Identity/Migrations/20240519151928_AddApplications.Designer.cs
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										320
									
								
								Foxchat.Identity/Migrations/20240519151928_AddApplications.Designer.cs
									
										
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,320 @@ | |||
| // <auto-generated /> | ||||
| using System; | ||||
| using Foxchat.Identity.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.Identity.Migrations | ||||
| { | ||||
|     [DbContext(typeof(IdentityContext))] | ||||
|     [Migration("20240519151928_AddApplications")] | ||||
|     partial class AddApplications | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void BuildTargetModel(ModelBuilder modelBuilder) | ||||
|         { | ||||
| #pragma warning disable 612, 618 | ||||
|             modelBuilder | ||||
|                 .HasAnnotation("ProductVersion", "8.0.4") | ||||
|                 .HasAnnotation("Relational:MaxIdentifierLength", 63); | ||||
| 
 | ||||
|             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); | ||||
| 
 | ||||
|             modelBuilder.Entity("AccountChatInstance", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("AccountsId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("accounts_id"); | ||||
| 
 | ||||
|                     b.Property<Guid>("ChatInstancesId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("chat_instances_id"); | ||||
| 
 | ||||
|                     b.HasKey("AccountsId", "ChatInstancesId") | ||||
|                         .HasName("pk_account_chat_instance"); | ||||
| 
 | ||||
|                     b.HasIndex("ChatInstancesId") | ||||
|                         .HasDatabaseName("ix_account_chat_instance_chat_instances_id"); | ||||
| 
 | ||||
|                     b.ToTable("account_chat_instance", (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("Foxchat.Identity.Database.Models.Account", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<string>("Avatar") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("avatar"); | ||||
| 
 | ||||
|                     b.Property<string>("Email") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("email"); | ||||
| 
 | ||||
|                     b.Property<string>("Password") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("password"); | ||||
| 
 | ||||
|                     b.Property<int>("Role") | ||||
|                         .HasColumnType("integer") | ||||
|                         .HasColumnName("role"); | ||||
| 
 | ||||
|                     b.Property<string>("Username") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("username"); | ||||
| 
 | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_accounts"); | ||||
| 
 | ||||
|                     b.HasIndex("Email") | ||||
|                         .IsUnique() | ||||
|                         .HasDatabaseName("ix_accounts_email"); | ||||
| 
 | ||||
|                     b.HasIndex("Username") | ||||
|                         .IsUnique() | ||||
|                         .HasDatabaseName("ix_accounts_username"); | ||||
| 
 | ||||
|                     b.ToTable("accounts", (string)null); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("Foxchat.Identity.Database.Models.Application", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<string>("ClientId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("client_id"); | ||||
| 
 | ||||
|                     b.Property<string>("ClientSecret") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("client_secret"); | ||||
| 
 | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("name"); | ||||
| 
 | ||||
|                     b.Property<string[]>("Scopes") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text[]") | ||||
|                         .HasColumnName("scopes"); | ||||
| 
 | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_applications"); | ||||
| 
 | ||||
|                     b.HasIndex("ClientId") | ||||
|                         .IsUnique() | ||||
|                         .HasDatabaseName("ix_applications_client_id"); | ||||
| 
 | ||||
|                     b.ToTable("applications", (string)null); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("Foxchat.Identity.Database.Models.ChatInstance", 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_chat_instances"); | ||||
| 
 | ||||
|                     b.HasIndex("Domain") | ||||
|                         .IsUnique() | ||||
|                         .HasDatabaseName("ix_chat_instances_domain"); | ||||
| 
 | ||||
|                     b.ToTable("chat_instances", (string)null); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("Foxchat.Identity.Database.Models.GuildAccount", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("ChatInstanceId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("chat_instance_id"); | ||||
| 
 | ||||
|                     b.Property<string>("GuildId") | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("guild_id"); | ||||
| 
 | ||||
|                     b.Property<Guid>("AccountId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("account_id"); | ||||
| 
 | ||||
|                     b.HasKey("ChatInstanceId", "GuildId", "AccountId") | ||||
|                         .HasName("pk_guild_accounts"); | ||||
| 
 | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_guild_accounts_account_id"); | ||||
| 
 | ||||
|                     b.ToTable("guild_accounts", (string)null); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("Foxchat.Identity.Database.Models.Token", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<Guid>("AccountId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("account_id"); | ||||
| 
 | ||||
|                     b.Property<Guid>("ApplicationId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("application_id"); | ||||
| 
 | ||||
|                     b.Property<Instant>("Expires") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("expires"); | ||||
| 
 | ||||
|                     b.Property<byte[]>("Hash") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("bytea") | ||||
|                         .HasColumnName("hash"); | ||||
| 
 | ||||
|                     b.Property<string[]>("Scopes") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text[]") | ||||
|                         .HasColumnName("scopes"); | ||||
| 
 | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_tokens"); | ||||
| 
 | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_tokens_account_id"); | ||||
| 
 | ||||
|                     b.HasIndex("ApplicationId") | ||||
|                         .HasDatabaseName("ix_tokens_application_id"); | ||||
| 
 | ||||
|                     b.ToTable("tokens", (string)null); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("AccountChatInstance", b => | ||||
|                 { | ||||
|                     b.HasOne("Foxchat.Identity.Database.Models.Account", null) | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AccountsId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_account_chat_instance_accounts_accounts_id"); | ||||
| 
 | ||||
|                     b.HasOne("Foxchat.Identity.Database.Models.ChatInstance", null) | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("ChatInstancesId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_account_chat_instance_chat_instances_chat_instances_id"); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("Foxchat.Identity.Database.Models.GuildAccount", b => | ||||
|                 { | ||||
|                     b.HasOne("Foxchat.Identity.Database.Models.Account", "Account") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("AccountId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_guild_accounts_accounts_account_id"); | ||||
| 
 | ||||
|                     b.HasOne("Foxchat.Identity.Database.Models.ChatInstance", "ChatInstance") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("ChatInstanceId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_guild_accounts_chat_instances_chat_instance_id"); | ||||
| 
 | ||||
|                     b.Navigation("Account"); | ||||
| 
 | ||||
|                     b.Navigation("ChatInstance"); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("Foxchat.Identity.Database.Models.Token", b => | ||||
|                 { | ||||
|                     b.HasOne("Foxchat.Identity.Database.Models.Account", "Account") | ||||
|                         .WithMany("Tokens") | ||||
|                         .HasForeignKey("AccountId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_tokens_accounts_account_id"); | ||||
| 
 | ||||
|                     b.HasOne("Foxchat.Identity.Database.Models.Application", "Application") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("ApplicationId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_tokens_applications_application_id"); | ||||
| 
 | ||||
|                     b.Navigation("Account"); | ||||
| 
 | ||||
|                     b.Navigation("Application"); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("Foxchat.Identity.Database.Models.Account", b => | ||||
|                 { | ||||
|                     b.Navigation("Tokens"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										109
									
								
								Foxchat.Identity/Migrations/20240519151928_AddApplications.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								Foxchat.Identity/Migrations/20240519151928_AddApplications.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,109 @@ | |||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
| 
 | ||||
| #nullable disable | ||||
| 
 | ||||
| namespace Foxchat.Identity.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddApplications : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AddColumn<Guid>( | ||||
|                 name: "application_id", | ||||
|                 table: "tokens", | ||||
|                 type: "uuid", | ||||
|                 nullable: false, | ||||
|                 defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); | ||||
| 
 | ||||
|             migrationBuilder.AddColumn<Instant>( | ||||
|                 name: "expires", | ||||
|                 table: "tokens", | ||||
|                 type: "timestamp with time zone", | ||||
|                 nullable: false, | ||||
|                 defaultValue: NodaTime.Instant.FromUnixTimeTicks(0L)); | ||||
| 
 | ||||
|             migrationBuilder.AddColumn<byte[]>( | ||||
|                 name: "hash", | ||||
|                 table: "tokens", | ||||
|                 type: "bytea", | ||||
|                 nullable: false, | ||||
|                 defaultValue: new byte[0]); | ||||
| 
 | ||||
|             migrationBuilder.AddColumn<string[]>( | ||||
|                 name: "scopes", | ||||
|                 table: "tokens", | ||||
|                 type: "text[]", | ||||
|                 nullable: false, | ||||
|                 defaultValue: new string[0]); | ||||
| 
 | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "applications", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     client_id = table.Column<string>(type: "text", nullable: false), | ||||
|                     client_secret = table.Column<string>(type: "text", nullable: false), | ||||
|                     name = table.Column<string>(type: "text", nullable: false), | ||||
|                     scopes = table.Column<string[]>(type: "text[]", nullable: false) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_applications", x => x.id); | ||||
|                 }); | ||||
| 
 | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_tokens_application_id", | ||||
|                 table: "tokens", | ||||
|                 column: "application_id"); | ||||
| 
 | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_applications_client_id", | ||||
|                 table: "applications", | ||||
|                 column: "client_id", | ||||
|                 unique: true); | ||||
| 
 | ||||
|             migrationBuilder.AddForeignKey( | ||||
|                 name: "fk_tokens_applications_application_id", | ||||
|                 table: "tokens", | ||||
|                 column: "application_id", | ||||
|                 principalTable: "applications", | ||||
|                 principalColumn: "id", | ||||
|                 onDelete: ReferentialAction.Cascade); | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropForeignKey( | ||||
|                 name: "fk_tokens_applications_application_id", | ||||
|                 table: "tokens"); | ||||
| 
 | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "applications"); | ||||
| 
 | ||||
|             migrationBuilder.DropIndex( | ||||
|                 name: "ix_tokens_application_id", | ||||
|                 table: "tokens"); | ||||
| 
 | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "application_id", | ||||
|                 table: "tokens"); | ||||
| 
 | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "expires", | ||||
|                 table: "tokens"); | ||||
| 
 | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "hash", | ||||
|                 table: "tokens"); | ||||
| 
 | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "scopes", | ||||
|                 table: "tokens"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -4,6 +4,7 @@ using Foxchat.Identity.Database; | |||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| using NodaTime; | ||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
| 
 | ||||
| #nullable disable | ||||
|  | @ -109,6 +110,42 @@ namespace Foxchat.Identity.Migrations | |||
|                     b.ToTable("accounts", (string)null); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("Foxchat.Identity.Database.Models.Application", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("id"); | ||||
| 
 | ||||
|                     b.Property<string>("ClientId") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("client_id"); | ||||
| 
 | ||||
|                     b.Property<string>("ClientSecret") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("client_secret"); | ||||
| 
 | ||||
|                     b.Property<string>("Name") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text") | ||||
|                         .HasColumnName("name"); | ||||
| 
 | ||||
|                     b.Property<string[]>("Scopes") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text[]") | ||||
|                         .HasColumnName("scopes"); | ||||
| 
 | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_applications"); | ||||
| 
 | ||||
|                     b.HasIndex("ClientId") | ||||
|                         .IsUnique() | ||||
|                         .HasDatabaseName("ix_applications_client_id"); | ||||
| 
 | ||||
|                     b.ToTable("applications", (string)null); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("Foxchat.Identity.Database.Models.ChatInstance", b => | ||||
|                 { | ||||
|                     b.Property<Guid>("Id") | ||||
|  | @ -181,12 +218,33 @@ namespace Foxchat.Identity.Migrations | |||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("account_id"); | ||||
| 
 | ||||
|                     b.Property<Guid>("ApplicationId") | ||||
|                         .HasColumnType("uuid") | ||||
|                         .HasColumnName("application_id"); | ||||
| 
 | ||||
|                     b.Property<Instant>("Expires") | ||||
|                         .HasColumnType("timestamp with time zone") | ||||
|                         .HasColumnName("expires"); | ||||
| 
 | ||||
|                     b.Property<byte[]>("Hash") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("bytea") | ||||
|                         .HasColumnName("hash"); | ||||
| 
 | ||||
|                     b.Property<string[]>("Scopes") | ||||
|                         .IsRequired() | ||||
|                         .HasColumnType("text[]") | ||||
|                         .HasColumnName("scopes"); | ||||
| 
 | ||||
|                     b.HasKey("Id") | ||||
|                         .HasName("pk_tokens"); | ||||
| 
 | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_tokens_account_id"); | ||||
| 
 | ||||
|                     b.HasIndex("ApplicationId") | ||||
|                         .HasDatabaseName("ix_tokens_application_id"); | ||||
| 
 | ||||
|                     b.ToTable("tokens", (string)null); | ||||
|                 }); | ||||
| 
 | ||||
|  | @ -237,7 +295,16 @@ namespace Foxchat.Identity.Migrations | |||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_tokens_accounts_account_id"); | ||||
| 
 | ||||
|                     b.HasOne("Foxchat.Identity.Database.Models.Application", "Application") | ||||
|                         .WithMany() | ||||
|                         .HasForeignKey("ApplicationId") | ||||
|                         .OnDelete(DeleteBehavior.Cascade) | ||||
|                         .IsRequired() | ||||
|                         .HasConstraintName("fk_tokens_applications_application_id"); | ||||
| 
 | ||||
|                     b.Navigation("Account"); | ||||
| 
 | ||||
|                     b.Navigation("Application"); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("Foxchat.Identity.Database.Models.Account", b => | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ var builder = WebApplication.CreateBuilder(args); | |||
| 
 | ||||
| var config = builder.AddConfiguration<InstanceConfig>("identity.ini"); | ||||
| 
 | ||||
| builder.Services.AddSerilog(config.LogEventLevel); | ||||
| builder.AddSerilog(config.LogEventLevel); | ||||
| 
 | ||||
| await BuildInfo.ReadBuildInfo(); | ||||
| Log.Information("Starting Foxchat.Identity {Version} ({Hash})", BuildInfo.Version, BuildInfo.Hash); | ||||
|  |  | |||
|  | @ -4,6 +4,8 @@ Domain = id.fox.localhost | |||
| 
 | ||||
| ; The level to log things at. Valid settings: Verbose, Debug, Information, Warning, Error, Fatal | ||||
| LogEventLevel = Debug | ||||
| ; Optional logging to Seq | ||||
| SeqLogUrl = http://localhost:5341 | ||||
| 
 | ||||
| [Database] | ||||
| ; The database URL in ADO.NET format. | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue