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