init
This commit is contained in:
		
						commit
						f6629fbb33
					
				
					 32 changed files with 1608 additions and 0 deletions
				
			
		
							
								
								
									
										4
									
								
								.editorconfig
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.editorconfig
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| [*.cs] | ||||
| 
 | ||||
| # CS9113: Parameter is unread. | ||||
| dotnet_diagnostic.CS9113.severity = silent | ||||
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| bin/ | ||||
| obj/ | ||||
| .version | ||||
							
								
								
									
										24
									
								
								Foxchat.Core/BuildInfo.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Foxchat.Core/BuildInfo.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| namespace Foxchat.Core; | ||||
| 
 | ||||
| public static class BuildInfo | ||||
| { | ||||
|     public static string Hash { get; private set; } = "(unknown)"; | ||||
|     public static string Version { get; private set; } = "(unknown)"; | ||||
| 
 | ||||
|     public static async Task ReadBuildInfo() | ||||
|     { | ||||
|         await using var stream = typeof(BuildInfo).Assembly.GetManifestResourceStream("version"); | ||||
|         if (stream == null) return; | ||||
| 
 | ||||
|         using var reader = new StreamReader(stream); | ||||
|         var data = (await reader.ReadToEndAsync()).Trim().Split("\n"); | ||||
| 
 | ||||
|         Hash = data[0]; | ||||
|         var dirty = data[2] == "dirty"; | ||||
| 
 | ||||
|         var versionData = data[1].Split("-"); | ||||
|         Version = versionData[0]; | ||||
|         if (versionData[1] != "0" || dirty) Version += $"+{versionData[2]}"; | ||||
|         if (dirty) Version += ".dirty"; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										24
									
								
								Foxchat.Core/CoreConfig.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Foxchat.Core/CoreConfig.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| using Serilog.Events; | ||||
| 
 | ||||
| namespace Foxchat.Core; | ||||
| 
 | ||||
| public class CoreConfig | ||||
| { | ||||
|     public string Host { get; set; } = "localhost"; | ||||
|     public int Port { get; set; } = 3000; | ||||
|     public bool Secure { get; set; } = false; | ||||
|     public string Domain { get; set; } = null!; | ||||
| 
 | ||||
|     public string Address => $"{(Secure ? "https" : "http")}://{Host}:{Port}"; | ||||
| 
 | ||||
|     public LogEventLevel LogEventLevel { get; set; } = LogEventLevel.Debug; | ||||
| 
 | ||||
|     public DatabaseConfig Database { get; set; } = new(); | ||||
| 
 | ||||
|     public class DatabaseConfig | ||||
|     { | ||||
|         public string Url { get; set; } = string.Empty; | ||||
|         public int? Timeout { get; set; } | ||||
|         public int? MaxPoolSize { get; set; } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										44
									
								
								Foxchat.Core/Database/IDatabaseContext.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								Foxchat.Core/Database/IDatabaseContext.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | |||
| using System.Security.Cryptography; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| 
 | ||||
| namespace Foxchat.Core.Database; | ||||
| 
 | ||||
| public abstract class IDatabaseContext : DbContext | ||||
| { | ||||
|     public virtual DbSet<Instance> Instance { get; set; } | ||||
| 
 | ||||
|     public async ValueTask<bool> InitializeInstanceAsync(CancellationToken ct = default) | ||||
|     { | ||||
|         var instance = await Instance.Where(i => i.Id == 1).FirstOrDefaultAsync(ct); | ||||
|         if (instance != null) return false; | ||||
| 
 | ||||
|         var rsa = RSA.Create(); | ||||
|         var publicKey = rsa.ExportRSAPublicKeyPem(); | ||||
|         var privateKey = rsa.ExportRSAPrivateKeyPem(); | ||||
| 
 | ||||
|         await Instance.AddAsync(new Instance | ||||
|         { | ||||
|             PublicKey = publicKey!, | ||||
|             PrivateKey = privateKey!, | ||||
|         }, ct); | ||||
| 
 | ||||
|         await SaveChangesAsync(ct); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<Instance> GetInstanceAsync(CancellationToken ct = default) | ||||
|     { | ||||
|         var instance = await Instance.FirstOrDefaultAsync(ct) | ||||
|             ?? throw new Exception("GetInstanceAsync called without Instance being initialized"); // TODO: replace this with specific exception type | ||||
|         return instance; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<RSA> GetInstanceKeysAsync(CancellationToken ct = default) | ||||
|     { | ||||
|         var instance = await GetInstanceAsync(ct); | ||||
| 
 | ||||
|         var rsa = RSA.Create(); | ||||
|         rsa.ImportFromPem(instance.PrivateKey); | ||||
|         return rsa; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										8
									
								
								Foxchat.Core/Database/Instance.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Foxchat.Core/Database/Instance.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| namespace Foxchat.Core.Database; | ||||
| 
 | ||||
| public class Instance | ||||
| { | ||||
|     public int Id { get; init; } | ||||
|     public string PublicKey { get; set; } = null!; | ||||
|     public string PrivateKey { get; set; } = null!; | ||||
| } | ||||
							
								
								
									
										72
									
								
								Foxchat.Core/Federation/RequestSigningService.Client.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								Foxchat.Core/Federation/RequestSigningService.Client.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,72 @@ | |||
| using System.Net.Http.Headers; | ||||
| using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Serialization; | ||||
| 
 | ||||
| namespace Foxchat.Core.Federation; | ||||
| 
 | ||||
| public partial class RequestSigningService | ||||
| { | ||||
|     public const string USER_AGENT_HEADER = "User-Agent"; | ||||
|     public const string USER_AGENT = "Foxchat.NET"; | ||||
|     public const string DATE_HEADER = "Date"; | ||||
|     public const string CONTENT_LENGTH_HEADER = "Content-Length"; | ||||
|     public const string CONTENT_TYPE_HEADER = "Content-Type"; | ||||
|     public const string CONTENT_TYPE = "application/json; charset=utf-8"; | ||||
| 
 | ||||
|     public const string SERVER_HEADER = "X-Foxchat-Server"; | ||||
|     public const string SIGNATURE_HEADER = "X-Foxchat-Signature"; | ||||
|     public const string USER_HEADER = "X-Foxchat-User"; | ||||
| 
 | ||||
|     private static readonly JsonSerializerSettings _jsonSerializerSettings = new() | ||||
|     { | ||||
|         ContractResolver = new DefaultContractResolver | ||||
|         { | ||||
|             NamingStrategy = new SnakeCaseNamingStrategy() | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     public async Task<T> RequestAsync<T>(HttpMethod method, string domain, string requestPath, string? userId = null, object? body = null) | ||||
|     { | ||||
|         var request = BuildHttpRequest(method, domain, requestPath, userId, body); | ||||
|         _logger.Debug("Content length in header: '{ContentLength}'", request.Headers.Where(c => c.Key == "Content-Length")); | ||||
|         var resp = await _httpClient.SendAsync(request); | ||||
|         if (!resp.IsSuccessStatusCode) | ||||
|         { | ||||
|             var error = await resp.Content.ReadAsStringAsync(); | ||||
|             _logger.Error("Received {Status}, body: {Error}", resp.StatusCode, error); | ||||
|             // TODO: replace this with specific exception type | ||||
|             throw new Exception("oh no a request error"); | ||||
|         } | ||||
| 
 | ||||
|         var bodyString = await resp.Content.ReadAsStringAsync(); | ||||
|         // TODO: replace this with specific exception type | ||||
|         return DeserializeObject<T>(bodyString) ?? throw new Exception("oh no invalid json"); | ||||
|     } | ||||
| 
 | ||||
|     private HttpRequestMessage BuildHttpRequest(HttpMethod method, string domain, string requestPath, string? userId = null, object? bodyData = null) | ||||
|     { | ||||
|         var body = bodyData != null ? SerializeObject(bodyData) : null; | ||||
| 
 | ||||
|         var now = _clock.GetCurrentInstant(); | ||||
|         var url = $"https://{domain}{requestPath}"; | ||||
|         var signature = GenerateSignature(new SignatureData(now, domain, requestPath, body?.Length, userId)); | ||||
| 
 | ||||
|         var request = new HttpRequestMessage(method, url); | ||||
|         request.Headers.Clear(); | ||||
|         request.Headers.Add(USER_AGENT_HEADER, USER_AGENT); | ||||
|         request.Headers.Add(DATE_HEADER, FormatTime(now)); | ||||
|         request.Headers.Add(SERVER_HEADER, _config.Domain); | ||||
|         request.Headers.Add(SIGNATURE_HEADER, signature); | ||||
|         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; | ||||
|     } | ||||
| 
 | ||||
|     public static string SerializeObject(object data) => JsonConvert.SerializeObject(data, _jsonSerializerSettings); | ||||
|     public static T? DeserializeObject<T>(string data) => JsonConvert.DeserializeObject<T>(data, _jsonSerializerSettings); | ||||
| } | ||||
							
								
								
									
										78
									
								
								Foxchat.Core/Federation/RequestSigningService.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								Foxchat.Core/Federation/RequestSigningService.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,78 @@ | |||
| using System.Globalization; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using Foxchat.Core.Database; | ||||
| using Microsoft.AspNetCore.WebUtilities; | ||||
| using NodaTime; | ||||
| using NodaTime.Text; | ||||
| using Serilog; | ||||
| 
 | ||||
| namespace Foxchat.Core.Federation; | ||||
| 
 | ||||
| public partial class RequestSigningService(ILogger logger, IClock clock, IDatabaseContext context, CoreConfig config) | ||||
| { | ||||
|     private readonly ILogger _logger = logger.ForContext<RequestSigningService>(); | ||||
|     private readonly IClock _clock = clock; | ||||
|     private readonly CoreConfig _config = config; | ||||
|     private readonly RSA _rsa = context.GetInstanceKeysAsync().GetAwaiter().GetResult(); | ||||
|     private readonly HttpClient _httpClient = new(); | ||||
| 
 | ||||
|     public string GenerateSignature(SignatureData data) | ||||
|     { | ||||
|         var plaintext = GeneratePlaintext(data); | ||||
|         var plaintextBytes = Encoding.UTF8.GetBytes(plaintext); | ||||
|         var hash = SHA256.HashData(plaintextBytes); | ||||
| 
 | ||||
|         var formatter = new RSAPKCS1SignatureFormatter(_rsa); | ||||
|         formatter.SetHashAlgorithm(nameof(SHA256)); | ||||
|         var signature = formatter.CreateSignature(hash); | ||||
| 
 | ||||
|         _logger.Debug("Generated signature for {Host} {RequestPath}: {Signature}", data.Host, data.RequestPath, WebEncoders.Base64UrlEncode(signature)); | ||||
|         return WebEncoders.Base64UrlEncode(signature); | ||||
|     } | ||||
| 
 | ||||
|     public bool VerifySignature( | ||||
|         string publicKey, string encodedSignature, string dateHeader, string host, string requestPath, int? contentLength, string? userId) | ||||
|     { | ||||
|         var rsa = RSA.Create(); | ||||
|         rsa.ImportFromPem(publicKey); | ||||
| 
 | ||||
|         var now = _clock.GetCurrentInstant(); | ||||
|         var time = ParseTime(dateHeader); | ||||
|         if ((now + Duration.FromMinutes(1)) < time) | ||||
|         { | ||||
|             // TODO: replace this with specific exception type | ||||
|             throw new Exception("Request was made in the future"); | ||||
|         } | ||||
|         else if ((now - Duration.FromMinutes(1)) > time) | ||||
|         { | ||||
|             // TODO: replace this with specific exception type | ||||
|             throw new Exception("Request was made too long ago"); | ||||
|         } | ||||
| 
 | ||||
|         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); | ||||
| 
 | ||||
|         var deformatter = new RSAPKCS1SignatureDeformatter(rsa); | ||||
|         deformatter.SetHashAlgorithm(nameof(SHA256)); | ||||
| 
 | ||||
|         return deformatter.VerifySignature(hash, signature); | ||||
|     } | ||||
| 
 | ||||
|     private static string GeneratePlaintext(SignatureData data) | ||||
|     { | ||||
|         var time = FormatTime(data.Time); | ||||
|         var contentLength = data.ContentLength != null ? data.ContentLength.ToString() : ""; | ||||
|         var userId = data.UserId ?? ""; | ||||
| 
 | ||||
|         Log.Information("Plaintext string: {Plaintext}", $"{time}:{data.Host}:{data.RequestPath}:{contentLength}:{userId}"); | ||||
| 
 | ||||
|         return $"{time}:{data.Host}:{data.RequestPath}:{contentLength}:{userId}"; | ||||
|     } | ||||
| 
 | ||||
|     private static readonly InstantPattern _pattern = InstantPattern.Create("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.GetCultureInfo("en-US")); | ||||
|     private static string FormatTime(Instant time) => _pattern.Format(time); | ||||
|     private static Instant ParseTime(string header) => _pattern.Parse(header).GetValueOrThrow(); | ||||
| } | ||||
							
								
								
									
										11
									
								
								Foxchat.Core/Federation/SignatureData.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								Foxchat.Core/Federation/SignatureData.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxchat.Core.Federation; | ||||
| 
 | ||||
| public record SignatureData( | ||||
|     Instant Time, | ||||
|     string Host, | ||||
|     string RequestPath, | ||||
|     int? ContentLength, | ||||
|     string? UserId | ||||
| ) { } | ||||
							
								
								
									
										27
									
								
								Foxchat.Core/Foxchat.Core.csproj
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Foxchat.Core/Foxchat.Core.csproj
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
| 
 | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>net8.0</TargetFramework> | ||||
|     <ImplicitUsings>enable</ImplicitUsings> | ||||
|     <Nullable>enable</Nullable> | ||||
|   </PropertyGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.4" /> | ||||
|     <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | ||||
|     <PackageReference Include="NodaTime" Version="3.1.11" /> | ||||
|     <PackageReference Include="NUlid" Version="1.7.2" /> | ||||
|     <PackageReference Include="Serilog" Version="3.1.1" /> | ||||
|     <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" /> | ||||
|     <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation"> | ||||
|     <Exec Command="../build_info.sh" IgnoreExitCode="false"> | ||||
|     </Exec> | ||||
|   </Target> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <EmbeddedResource Include="..\.version" LogicalName="version" /> | ||||
|   </ItemGroup> | ||||
| </Project> | ||||
							
								
								
									
										6
									
								
								Foxchat.Core/Models/Hello.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								Foxchat.Core/Models/Hello.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| namespace Foxchat.Core.Models; | ||||
| 
 | ||||
| public record HelloRequest(string Host); | ||||
| public record HelloResponse(string PublicKey, string Host); | ||||
| public record NodeInfo(string Software, string PublicKey); | ||||
| public record NodeSoftware(string Name, string? Version); | ||||
							
								
								
									
										75
									
								
								Foxchat.Core/ServiceCollectionExtensions.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								Foxchat.Core/ServiceCollectionExtensions.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,75 @@ | |||
| using Foxchat.Core.Database; | ||||
| using Foxchat.Core.Federation; | ||||
| using Microsoft.AspNetCore.Builder; | ||||
| using Microsoft.Extensions.Configuration; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using NodaTime; | ||||
| using Serilog; | ||||
| using Serilog.Events; | ||||
| using Serilog.Sinks.SystemConsole.Themes; | ||||
| 
 | ||||
| namespace Foxchat.Core; | ||||
| 
 | ||||
| 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) | ||||
|     { | ||||
|         var logCfg = new LoggerConfiguration() | ||||
|             .Enrich.FromLogContext() | ||||
|             .MinimumLevel.Is(level) | ||||
|             // ASP.NET's built in request logs are extremely verbose, so we use Serilog's instead. | ||||
|             // Serilog doesn't disable the built in logs so we do it here. | ||||
|             .MinimumLevel.Override("Microsoft", LogEventLevel.Information) | ||||
|             .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); | ||||
| 
 | ||||
|         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); | ||||
|     } | ||||
| 
 | ||||
|     /// <summary> | ||||
|     /// Adds the core Foxchat services to this service collection. | ||||
|     /// </summary> | ||||
|     public static IServiceCollection AddCoreServices<T>(this IServiceCollection services) where T : IDatabaseContext | ||||
|     { | ||||
|         services.AddDbContext<T>(); | ||||
| 
 | ||||
|         // NodaTime recommends only depending on the IClock interface, not the singleton. | ||||
|         services.AddSingleton<IClock>(SystemClock.Instance); | ||||
|         // Some core services rely on an IDatabaseContext, not the server-specific context type. | ||||
|         services.AddScoped<IDatabaseContext, T>(); | ||||
|         services.AddSingleton<RequestSigningService>(); | ||||
| 
 | ||||
|         return services; | ||||
|     } | ||||
| 
 | ||||
|     public static T AddConfiguration<T>(this WebApplicationBuilder builder, string? configFile = null) where T : class, new() | ||||
|     { | ||||
| 
 | ||||
|         builder.Configuration.Sources.Clear(); | ||||
|         builder.Configuration.AddConfiguration(configFile); | ||||
| 
 | ||||
|         var config = builder.Configuration.Get<T>() ?? new(); | ||||
|         var coreConfig = builder.Configuration.Get<CoreConfig>() ?? new(); | ||||
|         builder.Services.AddSingleton(config); | ||||
|         builder.Services.AddSingleton(coreConfig); | ||||
|         return config; | ||||
|     } | ||||
| 
 | ||||
|     public static IConfigurationBuilder AddConfiguration(this IConfigurationBuilder builder, string? configFile = null) | ||||
|     { | ||||
|         var file = Environment.GetEnvironmentVariable("FOXCHAT_CONFIG_FILE") ?? configFile ?? "config.ini"; | ||||
| 
 | ||||
|         return builder | ||||
|             .SetBasePath(Directory.GetCurrentDirectory()) | ||||
|             .AddIniFile(file, optional: false, reloadOnChange: true) | ||||
|             .AddEnvironmentVariables(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										10
									
								
								Foxchat.Core/UlidConverter.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								Foxchat.Core/UlidConverter.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| using NUlid; | ||||
| 
 | ||||
| namespace Foxchat.Core; | ||||
| 
 | ||||
| public class UlidConverter() : ValueConverter<Ulid, Guid>( | ||||
|     convertToProviderExpression: x => x.ToGuid(), | ||||
|     convertFromProviderExpression: x => new Ulid(x) | ||||
| ) | ||||
| { } | ||||
							
								
								
									
										9
									
								
								Foxchat.Identity/Authorization/AuthenticationHandler.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Foxchat.Identity/Authorization/AuthenticationHandler.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| namespace Foxchat.Identity.Authorization; | ||||
| 
 | ||||
| public static class AuthenticationHandlerExtensions | ||||
| { | ||||
|     public static void AddAuthenticationHandler(this IServiceCollection services) | ||||
|     { | ||||
| 
 | ||||
|     } | ||||
| } | ||||
							
								
								
									
										27
									
								
								Foxchat.Identity/Controllers/NodeController.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Foxchat.Identity/Controllers/NodeController.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| using Foxchat.Core.Models; | ||||
| using Foxchat.Identity.Database; | ||||
| using Foxchat.Identity.Services; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
| namespace Foxchat.Identity.Controllers; | ||||
| 
 | ||||
| [ApiController] | ||||
| [Route("/_fox/ident/node")] | ||||
| public class NodeController(IdentityContext context, ChatInstanceResolverService chatInstanceResolverService) : ControllerBase | ||||
| { | ||||
|     public const string SOFTWARE_NAME = "Foxchat.NET.Identity"; | ||||
| 
 | ||||
|     [HttpGet] | ||||
|     public async Task<IActionResult> GetNode() | ||||
|     { | ||||
|         var instance = await context.GetInstanceAsync(); | ||||
|         return Ok(new NodeInfo(SOFTWARE_NAME, instance.PublicKey)); | ||||
|     } | ||||
| 
 | ||||
|     [HttpGet("{domain}")] | ||||
|     public async Task<IActionResult> GetChatNode(string domain) | ||||
|     { | ||||
|         var instance = await chatInstanceResolverService.ResolveChatInstanceAsync(domain); | ||||
|         return Ok(instance); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										63
									
								
								Foxchat.Identity/Database/IdentityContext.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								Foxchat.Identity/Database/IdentityContext.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| using Foxchat.Core; | ||||
| using Foxchat.Core.Database; | ||||
| using Foxchat.Identity.Database.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Design; | ||||
| 
 | ||||
| namespace Foxchat.Identity.Database; | ||||
| 
 | ||||
| public class IdentityContext : IDatabaseContext | ||||
| { | ||||
|     private readonly string _connString; | ||||
| 
 | ||||
|     public DbSet<Account> Accounts { get; set; } | ||||
|     public DbSet<ChatInstance> ChatInstances { get; set; } | ||||
|     public override DbSet<Instance> Instance { 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) | ||||
|         { | ||||
|             Timeout = config.Database.Timeout ?? 5, | ||||
|             MaxPoolSize = config.Database.MaxPoolSize ?? 50, | ||||
|         }.ConnectionString; | ||||
|     } | ||||
| 
 | ||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
|             => optionsBuilder | ||||
|                 .UseNpgsql(_connString) | ||||
|                 .UseSnakeCaseNamingConvention(); | ||||
| 
 | ||||
|     protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) | ||||
|     { | ||||
|         // ULIDs are stored as UUIDs in the database | ||||
|         configurationBuilder.Properties<Ulid>().HaveConversion<UlidConverter>(); | ||||
|     } | ||||
| 
 | ||||
|     protected override void OnModelCreating(ModelBuilder modelBuilder) | ||||
|     { | ||||
|         modelBuilder.Entity<Account>().HasIndex(a => a.Username).IsUnique(); | ||||
|         modelBuilder.Entity<Account>().HasIndex(a => a.Email).IsUnique(); | ||||
| 
 | ||||
|         modelBuilder.Entity<ChatInstance>().HasIndex(i => i.Domain).IsUnique(); | ||||
| 
 | ||||
|         modelBuilder.Entity<GuildAccount>().HasKey(g => new { g.ChatInstanceId, g.GuildId, g.AccountId }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public class DesignTimeIdentityContextFactory : IDesignTimeDbContextFactory<IdentityContext> | ||||
| { | ||||
|     public IdentityContext CreateDbContext(string[] args) | ||||
|     { | ||||
|         // Read the configuration file | ||||
|         var config = new ConfigurationBuilder() | ||||
|             .AddConfiguration("identity.ini") | ||||
|             .Build() | ||||
|             // Get the configuration as our config class | ||||
|             .Get<InstanceConfig>() ?? new(); | ||||
| 
 | ||||
|         return new IdentityContext(config); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										21
									
								
								Foxchat.Identity/Database/Models/Account.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Foxchat.Identity/Database/Models/Account.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| namespace Foxchat.Identity.Database.Models; | ||||
| 
 | ||||
| public class Account | ||||
| { | ||||
|     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!; | ||||
|     public AccountRole Role { get; set; } | ||||
| 
 | ||||
|     public string? Avatar { get; set; } | ||||
| 
 | ||||
|     public List<Token> Tokens { get; } = []; | ||||
|     public List<ChatInstance> ChatInstances { get; } = []; | ||||
| 
 | ||||
|     public enum AccountRole | ||||
|     { | ||||
|         User, | ||||
|         Admin, | ||||
|     } | ||||
| } | ||||
							
								
								
									
										19
									
								
								Foxchat.Identity/Database/Models/ChatInstance.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								Foxchat.Identity/Database/Models/ChatInstance.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| namespace Foxchat.Identity.Database.Models; | ||||
| 
 | ||||
| public class ChatInstance | ||||
| { | ||||
|     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!; | ||||
|     public InstanceStatus Status { get; set; } | ||||
|     public string? Reason { get; set; } | ||||
| 
 | ||||
|     public List<Account> Accounts { get; } = []; | ||||
| 
 | ||||
|     public enum InstanceStatus | ||||
|     { | ||||
|         Active, | ||||
|         Suspended, | ||||
|     } | ||||
| } | ||||
							
								
								
									
										10
									
								
								Foxchat.Identity/Database/Models/GuildAccount.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								Foxchat.Identity/Database/Models/GuildAccount.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| namespace Foxchat.Identity.Database.Models; | ||||
| 
 | ||||
| public class GuildAccount | ||||
| { | ||||
|     public Ulid ChatInstanceId { get; init; } | ||||
|     public ChatInstance ChatInstance { get; init; } = null!; | ||||
|     public string GuildId { get; init; } = null!; | ||||
|     public Ulid AccountId { get; init; } | ||||
|     public Account Account { get; init; } = null!; | ||||
| } | ||||
							
								
								
									
										8
									
								
								Foxchat.Identity/Database/Models/Token.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Foxchat.Identity/Database/Models/Token.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| namespace Foxchat.Identity.Database.Models; | ||||
| 
 | ||||
| public class Token | ||||
| { | ||||
|     public Ulid Id { get; init; } = Ulid.NewUlid(); | ||||
|     public Ulid AccountId { get; set; } | ||||
|     public Account Account { get; set; } = null!; | ||||
| } | ||||
							
								
								
									
										29
									
								
								Foxchat.Identity/Foxchat.Identity.csproj
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								Foxchat.Identity/Foxchat.Identity.csproj
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||||
| 
 | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>net8.0</TargetFramework> | ||||
|     <Nullable>enable</Nullable> | ||||
|     <ImplicitUsings>enable</ImplicitUsings> | ||||
|   </PropertyGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="EFCore.NamingConventions" Version="8.0.3" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.4" /> | ||||
|     <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.4" /> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.4" /> | ||||
|     <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4"> | ||||
|       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|       <PrivateAssets>all</PrivateAssets> | ||||
|     </PackageReference> | ||||
|     <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | ||||
|     <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.2" /> | ||||
|     <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="Swashbuckle.AspNetCore" Version="6.4.0" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\Foxchat.Core\Foxchat.Core.csproj" /> | ||||
|   </ItemGroup> | ||||
| </Project> | ||||
							
								
								
									
										3
									
								
								Foxchat.Identity/GlobalUsing.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								Foxchat.Identity/GlobalUsing.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| global using ILogger = Serilog.ILogger; | ||||
| global using Log = Serilog.Log; | ||||
| global using NUlid; | ||||
							
								
								
									
										7
									
								
								Foxchat.Identity/InstanceConfig.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								Foxchat.Identity/InstanceConfig.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| using Foxchat.Core; | ||||
| 
 | ||||
| namespace Foxchat.Identity; | ||||
| 
 | ||||
| public class InstanceConfig : CoreConfig | ||||
| { | ||||
| } | ||||
							
								
								
									
										253
									
								
								Foxchat.Identity/Migrations/20240512225835_Init.Designer.cs
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										253
									
								
								Foxchat.Identity/Migrations/20240512225835_Init.Designer.cs
									
										
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,253 @@ | |||
| // <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 Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
| 
 | ||||
| #nullable disable | ||||
| 
 | ||||
| namespace Foxchat.Identity.Migrations | ||||
| { | ||||
|     [DbContext(typeof(IdentityContext))] | ||||
|     [Migration("20240512225835_Init")] | ||||
|     partial class Init | ||||
|     { | ||||
|         /// <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.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.HasKey("Id") | ||||
|                         .HasName("pk_tokens"); | ||||
| 
 | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_tokens_account_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.Navigation("Account"); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("Foxchat.Identity.Database.Models.Account", b => | ||||
|                 { | ||||
|                     b.Navigation("Tokens"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										181
									
								
								Foxchat.Identity/Migrations/20240512225835_Init.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								Foxchat.Identity/Migrations/20240512225835_Init.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,181 @@ | |||
| using System; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
| 
 | ||||
| #nullable disable | ||||
| 
 | ||||
| namespace Foxchat.Identity.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class Init : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "accounts", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     username = table.Column<string>(type: "text", nullable: false), | ||||
|                     email = table.Column<string>(type: "text", nullable: false), | ||||
|                     password = table.Column<string>(type: "text", nullable: false), | ||||
|                     role = table.Column<int>(type: "integer", nullable: false), | ||||
|                     avatar = table.Column<string>(type: "text", nullable: true) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_accounts", x => x.id); | ||||
|                 }); | ||||
| 
 | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "chat_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_chat_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: "tokens", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     account_id = table.Column<Guid>(type: "uuid", nullable: false) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_tokens", x => x.id); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_tokens_accounts_account_id", | ||||
|                         column: x => x.account_id, | ||||
|                         principalTable: "accounts", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                 }); | ||||
| 
 | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "account_chat_instance", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     accounts_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     chat_instances_id = table.Column<Guid>(type: "uuid", nullable: false) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_account_chat_instance", x => new { x.accounts_id, x.chat_instances_id }); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_account_chat_instance_accounts_accounts_id", | ||||
|                         column: x => x.accounts_id, | ||||
|                         principalTable: "accounts", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_account_chat_instance_chat_instances_chat_instances_id", | ||||
|                         column: x => x.chat_instances_id, | ||||
|                         principalTable: "chat_instances", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                 }); | ||||
| 
 | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "guild_accounts", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     chat_instance_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     guild_id = table.Column<string>(type: "text", nullable: false), | ||||
|                     account_id = table.Column<Guid>(type: "uuid", nullable: false) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_guild_accounts", x => new { x.chat_instance_id, x.guild_id, x.account_id }); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_guild_accounts_accounts_account_id", | ||||
|                         column: x => x.account_id, | ||||
|                         principalTable: "accounts", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_guild_accounts_chat_instances_chat_instance_id", | ||||
|                         column: x => x.chat_instance_id, | ||||
|                         principalTable: "chat_instances", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                 }); | ||||
| 
 | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_account_chat_instance_chat_instances_id", | ||||
|                 table: "account_chat_instance", | ||||
|                 column: "chat_instances_id"); | ||||
| 
 | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_accounts_email", | ||||
|                 table: "accounts", | ||||
|                 column: "email", | ||||
|                 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_accounts_username ON accounts (lower(username))"); | ||||
| 
 | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_chat_instances_domain", | ||||
|                 table: "chat_instances", | ||||
|                 column: "domain", | ||||
|                 unique: true); | ||||
| 
 | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_guild_accounts_account_id", | ||||
|                 table: "guild_accounts", | ||||
|                 column: "account_id"); | ||||
| 
 | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_tokens_account_id", | ||||
|                 table: "tokens", | ||||
|                 column: "account_id"); | ||||
|         } | ||||
| 
 | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "account_chat_instance"); | ||||
| 
 | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "guild_accounts"); | ||||
| 
 | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "instance"); | ||||
| 
 | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "tokens"); | ||||
| 
 | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "chat_instances"); | ||||
| 
 | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "accounts"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										250
									
								
								Foxchat.Identity/Migrations/IdentityContextModelSnapshot.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								Foxchat.Identity/Migrations/IdentityContextModelSnapshot.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,250 @@ | |||
| // <auto-generated /> | ||||
| using System; | ||||
| using Foxchat.Identity.Database; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; | ||||
| 
 | ||||
| #nullable disable | ||||
| 
 | ||||
| namespace Foxchat.Identity.Migrations | ||||
| { | ||||
|     [DbContext(typeof(IdentityContext))] | ||||
|     partial class IdentityContextModelSnapshot : ModelSnapshot | ||||
|     { | ||||
|         protected override void BuildModel(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.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.HasKey("Id") | ||||
|                         .HasName("pk_tokens"); | ||||
| 
 | ||||
|                     b.HasIndex("AccountId") | ||||
|                         .HasDatabaseName("ix_tokens_account_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.Navigation("Account"); | ||||
|                 }); | ||||
| 
 | ||||
|             modelBuilder.Entity("Foxchat.Identity.Database.Models.Account", b => | ||||
|                 { | ||||
|                     b.Navigation("Tokens"); | ||||
|                 }); | ||||
| #pragma warning restore 612, 618 | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										55
									
								
								Foxchat.Identity/Program.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								Foxchat.Identity/Program.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | |||
| using Newtonsoft.Json.Serialization; | ||||
| using Serilog; | ||||
| using Foxchat.Core; | ||||
| using Foxchat.Identity; | ||||
| using Foxchat.Identity.Database; | ||||
| using Foxchat.Identity.Services; | ||||
| 
 | ||||
| var builder = WebApplication.CreateBuilder(args); | ||||
| 
 | ||||
| var config = builder.AddConfiguration<InstanceConfig>("identity.ini"); | ||||
| 
 | ||||
| builder.Services.AddSerilog(config.LogEventLevel); | ||||
| 
 | ||||
| await BuildInfo.ReadBuildInfo(); | ||||
| Log.Information("Starting Foxchat.Identity {Version} ({Hash})", BuildInfo.Version, BuildInfo.Hash); | ||||
| 
 | ||||
| builder.Services | ||||
|     .AddControllers() | ||||
|     .AddNewtonsoftJson(options => | ||||
|         options.SerializerSettings.ContractResolver = new DefaultContractResolver | ||||
|         { | ||||
|             NamingStrategy = new SnakeCaseNamingStrategy() | ||||
|         }); | ||||
| 
 | ||||
| builder.Services | ||||
|     .AddCoreServices<IdentityContext>() | ||||
|     .AddScoped<ChatInstanceResolverService>() | ||||
|     .AddEndpointsApiExplorer() | ||||
|     .AddSwaggerGen(); | ||||
| 
 | ||||
| var app = builder.Build(); | ||||
| 
 | ||||
| app.UseSerilogRequestLogging(); | ||||
| app.UseRouting(); | ||||
| app.UseSwagger(); | ||||
| app.UseSwaggerUI(); | ||||
| app.UseCors(); | ||||
| app.UseAuthentication(); | ||||
| app.UseAuthorization(); | ||||
| app.MapControllers(); | ||||
| 
 | ||||
| using (var scope = app.Services.CreateScope()) | ||||
| using (var context = scope.ServiceProvider.GetRequiredService<IdentityContext>()) | ||||
| { | ||||
|     Log.Information("Initializing instance keypair..."); | ||||
|     if (await context.InitializeInstanceAsync()) | ||||
|     { | ||||
|         Log.Information("Initialized instance keypair"); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| app.Urls.Clear(); | ||||
| app.Urls.Add(config.Address); | ||||
| 
 | ||||
| app.Run(); | ||||
							
								
								
									
										39
									
								
								Foxchat.Identity/Services/ChatInstanceResolverService.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								Foxchat.Identity/Services/ChatInstanceResolverService.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| using Foxchat.Core.Federation; | ||||
| using Foxchat.Core.Models; | ||||
| using Foxchat.Identity.Database; | ||||
| using Foxchat.Identity.Database.Models; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| 
 | ||||
| namespace Foxchat.Identity.Services; | ||||
| 
 | ||||
| public class ChatInstanceResolverService(ILogger logger, RequestSigningService requestSigningService, IdentityContext context, InstanceConfig config) | ||||
| { | ||||
|     private readonly ILogger _logger = logger.ForContext<ChatInstanceResolverService>(); | ||||
| 
 | ||||
|     public async Task<ChatInstance> ResolveChatInstanceAsync(string domain) | ||||
|     { | ||||
|         var instance = await context.ChatInstances.Where(c => c.Domain == domain).FirstOrDefaultAsync(); | ||||
|         if (instance != null) return instance; | ||||
| 
 | ||||
|         _logger.Information("Unknown chat instance {Domain}, fetching its data", domain); | ||||
| 
 | ||||
|         var resp = await requestSigningService.RequestAsync<HelloResponse>( | ||||
|             HttpMethod.Post, | ||||
|             domain, "/_fox/chat/hello", | ||||
|             userId: null, | ||||
|             body: new HelloRequest(config.Domain) | ||||
|         ); | ||||
| 
 | ||||
|         instance = new ChatInstance | ||||
|         { | ||||
|             Domain = domain, | ||||
|             BaseUrl = $"https://{domain}", | ||||
|             PublicKey = resp.PublicKey, | ||||
|             Status = ChatInstance.InstanceStatus.Active, | ||||
|         }; | ||||
|         await context.AddAsync(instance); | ||||
|         await context.SaveChangesAsync(); | ||||
| 
 | ||||
|         return instance; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										15
									
								
								Foxchat.Identity/identity.ini
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Foxchat.Identity/identity.ini
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| Host = localhost | ||||
| Port = 7611 | ||||
| Domain = id.fox.localhost | ||||
| 
 | ||||
| ; The level to log things at. Valid settings: Verbose, Debug, Information, Warning, Error, Fatal | ||||
| LogEventLevel = Debug | ||||
| 
 | ||||
| [Database] | ||||
| ; The database URL in ADO.NET format. | ||||
| Url = "Host=localhost;Database=foxchat_cs_ident;Username=foxchat;Password=password" | ||||
| 
 | ||||
| ; The timeout for opening new connections. Defaults to 5. | ||||
| Timeout = 5 | ||||
| ; The maximum number of open connections. Defaults to 50. | ||||
| MaxPoolSize = 500 | ||||
							
								
								
									
										28
									
								
								Foxchat.sln
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Foxchat.sln
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | |||
|  | ||||
| Microsoft Visual Studio Solution File, Format Version 12.00 | ||||
| # Visual Studio Version 17 | ||||
| VisualStudioVersion = 17.0.31903.59 | ||||
| MinimumVisualStudioVersion = 10.0.40219.1 | ||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foxchat.Identity", "Foxchat.Identity\Foxchat.Identity.csproj", "{29265F09-5312-41B5-86C4-3B9EBF155F93}" | ||||
| EndProject | ||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foxchat.Core", "Foxchat.Core\Foxchat.Core.csproj", "{06352B8B-628C-4476-9F78-83F326B05B16}" | ||||
| EndProject | ||||
| Global | ||||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
| 		Debug|Any CPU = Debug|Any CPU | ||||
| 		Release|Any CPU = Release|Any CPU | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(SolutionProperties) = preSolution | ||||
| 		HideSolutionNode = FALSE | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||||
| 		{29265F09-5312-41B5-86C4-3B9EBF155F93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{29265F09-5312-41B5-86C4-3B9EBF155F93}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{29265F09-5312-41B5-86C4-3B9EBF155F93}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{29265F09-5312-41B5-86C4-3B9EBF155F93}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 		{06352B8B-628C-4476-9F78-83F326B05B16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{06352B8B-628C-4476-9F78-83F326B05B16}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{06352B8B-628C-4476-9F78-83F326B05B16}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{06352B8B-628C-4476-9F78-83F326B05B16}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 	EndGlobalSection | ||||
| EndGlobal | ||||
							
								
								
									
										201
									
								
								LICENSE
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								LICENSE
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,201 @@ | |||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
| 
 | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
| 
 | ||||
|    1. Definitions. | ||||
| 
 | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
| 
 | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
| 
 | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
| 
 | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
| 
 | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
| 
 | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
| 
 | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
| 
 | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
| 
 | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
| 
 | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
| 
 | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
| 
 | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
| 
 | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
| 
 | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
| 
 | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
| 
 | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
| 
 | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
| 
 | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
| 
 | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
| 
 | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
| 
 | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
| 
 | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
| 
 | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
| 
 | ||||
|    END OF TERMS AND CONDITIONS | ||||
| 
 | ||||
|    APPENDIX: How to apply the Apache License to your work. | ||||
| 
 | ||||
|       To apply the Apache License to your work, attach the following | ||||
|       boilerplate notice, with the fields enclosed by brackets "[]" | ||||
|       replaced with your own identifying information. (Don't include | ||||
|       the brackets!)  The text should be enclosed in the appropriate | ||||
|       comment syntax for the file format. We also recommend that a | ||||
|       file or class name and description of purpose be included on the | ||||
|       same "printed page" as the copyright notice for easier | ||||
|       identification within third-party archives. | ||||
| 
 | ||||
|    Copyright [yyyy] [name of copyright owner] | ||||
| 
 | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
| 
 | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
| 
 | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
							
								
								
									
										4
									
								
								build_info.sh
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								build_info.sh
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| #!/bin/sh | ||||
| (git rev-parse HEAD && | ||||
|   git describe --tags --long && | ||||
|   if test -z "$(git ls-files --exclude-standard --modified --deleted --others)"; then echo clean; else echo dirty; fi) > ../.version | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue