diff --git a/DysonNetwork.Sphere/AppDatabase.cs b/DysonNetwork.Sphere/AppDatabase.cs index 568ca00..cdcaca0 100644 --- a/DysonNetwork.Sphere/AppDatabase.cs +++ b/DysonNetwork.Sphere/AppDatabase.cs @@ -196,6 +196,41 @@ public class AppDatabase( .HasGeneratedTsVectorColumn(p => p.SearchVector, "simple", p => new { p.Title, p.Description, p.Content }) .HasIndex(p => p.SearchVector) .HasMethod("GIN"); + + modelBuilder.Entity() + .Property(c => c.RedirectUris) + .HasConversion( + v => string.Join(",", v), + v => v.Split(",", StringSplitOptions.RemoveEmptyEntries).ToArray()); + + modelBuilder.Entity() + .Property(c => c.PostLogoutRedirectUris) + .HasConversion( + v => v != null ? string.Join(",", v) : "", + v => !string.IsNullOrEmpty(v) ? v.Split(",", StringSplitOptions.RemoveEmptyEntries) : Array.Empty()); + + modelBuilder.Entity() + .Property(c => c.AllowedScopes) + .HasConversion( + v => v != null ? string.Join(" ", v) : "", + v => !string.IsNullOrEmpty(v) ? v.Split(" ", StringSplitOptions.RemoveEmptyEntries) : new[] { "openid", "profile", "email" }); + + modelBuilder.Entity() + .Property(c => c.AllowedGrantTypes) + .HasConversion( + v => v != null ? string.Join(" ", v) : "", + v => !string.IsNullOrEmpty(v) ? v.Split(" ", StringSplitOptions.RemoveEmptyEntries) : new[] { "authorization_code", "refresh_token" }); + + modelBuilder.Entity() + .HasIndex(s => s.Secret) + .IsUnique(); + + modelBuilder.Entity() + .HasMany(c => c.Secrets) + .WithOne(s => s.App) + .HasForeignKey(s => s.AppId) + .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() .HasOne(p => p.RepliedPost) .WithMany() diff --git a/DysonNetwork.Sphere/Auth/OidcProvider/Controllers/OidcProviderController.cs b/DysonNetwork.Sphere/Auth/OidcProvider/Controllers/OidcProviderController.cs new file mode 100644 index 0000000..910b7c5 --- /dev/null +++ b/DysonNetwork.Sphere/Auth/OidcProvider/Controllers/OidcProviderController.cs @@ -0,0 +1,305 @@ +using System.ComponentModel.DataAnnotations; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using DysonNetwork.Sphere.Developer; +using DysonNetwork.Sphere.Auth.OidcProvider.Options; +using DysonNetwork.Sphere.Auth.OidcProvider.Responses; +using DysonNetwork.Sphere.Auth.OidcProvider.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using System.Text.Json.Serialization; + +namespace DysonNetwork.Sphere.Auth.OidcProvider.Controllers; + +[Route("connect")] +[ApiController] +public class OidcProviderController( + OidcProviderService oidcService, + IOptions options, + ILogger logger +) + : ControllerBase +{ + [HttpGet("authorize")] + public async Task Authorize( + [Required][FromQuery(Name = "client_id")] Guid clientId, + [Required][FromQuery(Name = "response_type")] string responseType, + [FromQuery(Name = "redirect_uri")] string? redirectUri, + [FromQuery] string? scope, + [FromQuery] string? state, + [FromQuery] string? nonce, + [FromQuery(Name = "code_challenge")] string? codeChallenge, + [FromQuery(Name = "code_challenge_method")] string? codeChallengeMethod, + [FromQuery(Name = "response_mode")] string? responseMode + ) + { + // Check if user is authenticated + if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) + { + // Not authenticated - redirect to login with return URL + var loginUrl = "/Auth/Login"; + var returnUrl = $"{Request.Path}{Request.QueryString}"; + return Redirect($"{loginUrl}?returnUrl={Uri.EscapeDataString(returnUrl)}"); + } + + // Validate client + var client = await oidcService.FindClientByIdAsync(clientId); + if (client == null) + return BadRequest(new ErrorResponse { Error = "invalid_client", ErrorDescription = "Client not found" }); + + // Check if user has already granted permission to this client + // For now, we'll always show the consent page. In a real app, you might store consent decisions. + // If you want to implement "remember my decision", you would check that here. + var consentRequired = true; + + if (consentRequired) + { + // Redirect to consent page with all the OAuth parameters + var consentUrl = $"/Auth/Authorize?client_id={clientId}"; + if (!string.IsNullOrEmpty(responseType)) consentUrl += $"&response_type={Uri.EscapeDataString(responseType)}"; + if (!string.IsNullOrEmpty(redirectUri)) consentUrl += $"&redirect_uri={Uri.EscapeDataString(redirectUri)}"; + if (!string.IsNullOrEmpty(scope)) consentUrl += $"&scope={Uri.EscapeDataString(scope)}"; + if (!string.IsNullOrEmpty(state)) consentUrl += $"&state={Uri.EscapeDataString(state)}"; + if (!string.IsNullOrEmpty(nonce)) consentUrl += $"&nonce={Uri.EscapeDataString(nonce)}"; + if (!string.IsNullOrEmpty(codeChallenge)) consentUrl += $"&code_challenge={Uri.EscapeDataString(codeChallenge)}"; + if (!string.IsNullOrEmpty(codeChallengeMethod)) consentUrl += $"&code_challenge_method={Uri.EscapeDataString(codeChallengeMethod)}"; + if (!string.IsNullOrEmpty(responseMode)) consentUrl += $"&response_mode={Uri.EscapeDataString(responseMode)}"; + + return Redirect(consentUrl); + } + + // Skip redirect_uri validation for apps in Developing status + if (client.Status != CustomAppStatus.Developing) + { + // Validate redirect URI for non-Developing apps + if (!string.IsNullOrEmpty(redirectUri) && !(client.RedirectUris?.Contains(redirectUri) ?? false)) + return BadRequest( + new ErrorResponse { Error = "invalid_request", ErrorDescription = "Invalid redirect_uri" }); + } + else + { + logger.LogWarning("Skipping redirect_uri validation for app {AppId} in Developing status", clientId); + + // If no redirect_uri is provided and we're in development, use the first one + if (string.IsNullOrEmpty(redirectUri) && client.RedirectUris?.Any() == true) + { + redirectUri = client.RedirectUris.First(); + } + } + + // Generate authorization code + var code = Guid.NewGuid().ToString("N"); + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + // In a real implementation, you'd store this code with the user's consent and requested scopes + // and validate it in the token endpoint + + // For now, we'll just return the code directly (simplified for example) + var response = new AuthorizationResponse + { + Code = code, + State = state, + Scope = scope, + Issuer = options.Value.IssuerUri + }; + + // Redirect back to the client with the authorization code + var finalRedirectUri = new UriBuilder(redirectUri ?? client.RedirectUris?.First() ?? throw new InvalidOperationException("No redirect URI provided and no default redirect URI found")); + var query = System.Web.HttpUtility.ParseQueryString(finalRedirectUri.Query); + query["code"] = response.Code; + if (!string.IsNullOrEmpty(response.State)) + query["state"] = response.State; + if (!string.IsNullOrEmpty(response.Scope)) + query["scope"] = response.Scope; + + finalRedirectUri.Query = query.ToString(); + return Redirect(finalRedirectUri.Uri.ToString()); + } + + [HttpPost("token")] + [Consumes("application/x-www-form-urlencoded")] + public async Task Token([FromForm] TokenRequest request) + { + if (request.GrantType == "authorization_code") + { + // Validate client credentials + if (request.ClientId == null || string.IsNullOrEmpty(request.ClientSecret)) + return BadRequest(new ErrorResponse { Error = "invalid_client", ErrorDescription = "Client credentials are required" }); + + var client = await oidcService.FindClientByIdAsync(request.ClientId.Value); + if (client == null || !await oidcService.ValidateClientCredentialsAsync(request.ClientId.Value, request.ClientSecret)) + return BadRequest(new ErrorResponse { Error = "invalid_client", ErrorDescription = "Invalid client credentials" }); + + // Validate the authorization code + var authCode = await oidcService.ValidateAuthorizationCodeAsync( + request.Code ?? string.Empty, + request.ClientId.Value, + request.RedirectUri, + request.CodeVerifier); + + if (authCode == null) + { + logger.LogWarning("Invalid or expired authorization code: {Code}", request.Code); + return BadRequest(new ErrorResponse { Error = "invalid_grant", ErrorDescription = "Invalid or expired authorization code" }); + } + + // Generate tokens + var tokenResponse = await oidcService.GenerateTokenResponseAsync( + clientId: request.ClientId.Value, + subjectId: authCode.UserId, + scopes: authCode.Scopes, + authorizationCode: request.Code); + + return Ok(tokenResponse); + } + else if (request.GrantType == "refresh_token") + { + // Handle refresh token request + // In a real implementation, you would validate the refresh token + // and issue a new access token + return BadRequest(new ErrorResponse { Error = "unsupported_grant_type" }); + } + + return BadRequest(new ErrorResponse { Error = "unsupported_grant_type" }); + } + + [HttpGet("userinfo")] + [Authorize(AuthenticationSchemes = "Bearer")] + public async Task UserInfo() + { + var authHeader = HttpContext.Request.Headers.Authorization.ToString(); + if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ")) + { + var loginUrl = "/Account/Login"; // Update this path to your actual login page path + var returnUrl = $"{Request.Scheme}://{Request.Host}{Request.Path}{Request.QueryString}"; + return Redirect($"{loginUrl}?returnUrl={Uri.EscapeDataString(returnUrl)}"); + } + + var token = authHeader["Bearer ".Length..].Trim(); + var jwtToken = oidcService.ValidateToken(token); + + if (jwtToken == null) + { + var loginUrl = "/Account/Login"; // Update this path to your actual login page path + var returnUrl = $"{Request.Scheme}://{Request.Host}{Request.Path}{Request.QueryString}"; + return Redirect($"{loginUrl}?returnUrl={Uri.EscapeDataString(returnUrl)}"); + } + + // Get user info based on the subject claim from the token + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + var userName = User.FindFirstValue(ClaimTypes.Name); + var userEmail = User.FindFirstValue(ClaimTypes.Email); + + // Get requested scopes from the token + var scopes = jwtToken.Claims + .Where(c => c.Type == "scope") + .SelectMany(c => c.Value.Split(' ')) + .ToHashSet(); + + var userInfo = new Dictionary + { + ["sub"] = userId ?? "anonymous" + }; + + // Include standard claims based on scopes + if (scopes.Contains("profile") || scopes.Contains("name")) + { + if (!string.IsNullOrEmpty(userName)) + userInfo["name"] = userName; + } + + if (scopes.Contains("email") && !string.IsNullOrEmpty(userEmail)) + { + userInfo["email"] = userEmail; + userInfo["email_verified"] = true; // In a real app, check if email is verified + } + + return Ok(userInfo); + } + + [HttpGet(".well-known/openid-configuration")] + public IActionResult GetConfiguration() + { + var baseUrl = $"{Request.Scheme}://{Request.Host}{Request.PathBase}".TrimEnd('/'); + var issuer = options.Value.IssuerUri.TrimEnd('/'); + + return Ok(new + { + issuer = issuer, + authorization_endpoint = $"{baseUrl}/connect/authorize", + token_endpoint = $"{baseUrl}/connect/token", + userinfo_endpoint = $"{baseUrl}/connect/userinfo", + jwks_uri = $"{baseUrl}/.well-known/openid-configuration/jwks", + scopes_supported = new[] { "openid", "profile", "email" }, + response_types_supported = new[] { "code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token" }, + grant_types_supported = new[] { "authorization_code", "refresh_token" }, + token_endpoint_auth_methods_supported = new[] { "client_secret_basic", "client_secret_post" }, + id_token_signing_alg_values_supported = new[] { "HS256" }, + subject_types_supported = new[] { "public" }, + claims_supported = new[] { "sub", "name", "email", "email_verified" }, + code_challenge_methods_supported = new[] { "S256" }, + response_modes_supported = new[] { "query", "fragment", "form_post" }, + request_parameter_supported = true, + request_uri_parameter_supported = true, + require_request_uri_registration = false + }); + } + + [HttpGet("jwks")] + public IActionResult Jwks() + { + // In a production environment, you should use asymmetric keys (RSA or EC) + // and expose only the public key here. This is a simplified example using HMAC. + // For production, consider using RSA or EC keys and proper key rotation. + + var keyBytes = Encoding.UTF8.GetBytes(options.Value.SigningKey); + var keyId = Convert.ToBase64String(SHA256.HashData(keyBytes)[..8]) + .Replace("+", "-") + .Replace("/", "_") + .Replace("=", ""); + + return Ok(new + { + keys = new[] + { + new + { + kty = "oct", + use = "sig", + kid = keyId, + k = Convert.ToBase64String(keyBytes), + alg = "HS256" + } + } + }); + } +} + +public class TokenRequest +{ + [JsonPropertyName("grant_type")] + public string? GrantType { get; set; } + + [JsonPropertyName("code")] + public string? Code { get; set; } + + [JsonPropertyName("redirect_uri")] + public string? RedirectUri { get; set; } + + [JsonPropertyName("client_id")] + public Guid? ClientId { get; set; } + + [JsonPropertyName("client_secret")] + public string? ClientSecret { get; set; } + + [JsonPropertyName("refresh_token")] + public string? RefreshToken { get; set; } + + [JsonPropertyName("scope")] + public string? Scope { get; set; } + + [JsonPropertyName("code_verifier")] + public string? CodeVerifier { get; set; } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Auth/OidcProvider/Models/AuthorizationCodeInfo.cs b/DysonNetwork.Sphere/Auth/OidcProvider/Models/AuthorizationCodeInfo.cs new file mode 100644 index 0000000..7e3795a --- /dev/null +++ b/DysonNetwork.Sphere/Auth/OidcProvider/Models/AuthorizationCodeInfo.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using NodaTime; + +namespace DysonNetwork.Sphere.Auth.OidcProvider.Models; + +public class AuthorizationCodeInfo +{ + public Guid ClientId { get; set; } + public string UserId { get; set; } = string.Empty; + public string RedirectUri { get; set; } = string.Empty; + public List Scopes { get; set; } = new(); + public string? CodeChallenge { get; set; } + public string? CodeChallengeMethod { get; set; } + public string? Nonce { get; set; } + public Instant Expiration { get; set; } + public Instant CreatedAt { get; set; } +} diff --git a/DysonNetwork.Sphere/Auth/OidcProvider/Options/OidcProviderOptions.cs b/DysonNetwork.Sphere/Auth/OidcProvider/Options/OidcProviderOptions.cs new file mode 100644 index 0000000..c886d9d --- /dev/null +++ b/DysonNetwork.Sphere/Auth/OidcProvider/Options/OidcProviderOptions.cs @@ -0,0 +1,13 @@ +using System; + +namespace DysonNetwork.Sphere.Auth.OidcProvider.Options; + +public class OidcProviderOptions +{ + public string IssuerUri { get; set; } = "https://your-issuer-uri.com"; + public string SigningKey { get; set; } = "replace-with-a-secure-random-key"; + public TimeSpan AccessTokenLifetime { get; set; } = TimeSpan.FromHours(1); + public TimeSpan RefreshTokenLifetime { get; set; } = TimeSpan.FromDays(30); + public TimeSpan AuthorizationCodeLifetime { get; set; } = TimeSpan.FromMinutes(5); + public bool RequireHttpsMetadata { get; set; } = true; +} diff --git a/DysonNetwork.Sphere/Auth/OidcProvider/Responses/AuthorizationResponse.cs b/DysonNetwork.Sphere/Auth/OidcProvider/Responses/AuthorizationResponse.cs new file mode 100644 index 0000000..45cf25c --- /dev/null +++ b/DysonNetwork.Sphere/Auth/OidcProvider/Responses/AuthorizationResponse.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses; + +public class AuthorizationResponse +{ + [JsonPropertyName("code")] + public string Code { get; set; } = null!; + + [JsonPropertyName("state")] + public string? State { get; set; } + + [JsonPropertyName("scope")] + public string? Scope { get; set; } + + + [JsonPropertyName("session_state")] + public string? SessionState { get; set; } + + + [JsonPropertyName("iss")] + public string? Issuer { get; set; } +} diff --git a/DysonNetwork.Sphere/Auth/OidcProvider/Responses/ErrorResponse.cs b/DysonNetwork.Sphere/Auth/OidcProvider/Responses/ErrorResponse.cs new file mode 100644 index 0000000..0018a47 --- /dev/null +++ b/DysonNetwork.Sphere/Auth/OidcProvider/Responses/ErrorResponse.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses; + +public class ErrorResponse +{ + [JsonPropertyName("error")] + public string Error { get; set; } = null!; + + [JsonPropertyName("error_description")] + public string? ErrorDescription { get; set; } + + + [JsonPropertyName("error_uri")] + public string? ErrorUri { get; set; } + + + [JsonPropertyName("state")] + public string? State { get; set; } +} diff --git a/DysonNetwork.Sphere/Auth/OidcProvider/Responses/TokenResponse.cs b/DysonNetwork.Sphere/Auth/OidcProvider/Responses/TokenResponse.cs new file mode 100644 index 0000000..6d41cf4 --- /dev/null +++ b/DysonNetwork.Sphere/Auth/OidcProvider/Responses/TokenResponse.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +namespace DysonNetwork.Sphere.Auth.OidcProvider.Responses; + +public class TokenResponse +{ + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } = null!; + + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + + [JsonPropertyName("token_type")] + public string TokenType { get; set; } = "Bearer"; + + [JsonPropertyName("refresh_token")] + public string? RefreshToken { get; set; } + + + [JsonPropertyName("scope")] + public string? Scope { get; set; } + + + [JsonPropertyName("id_token")] + public string? IdToken { get; set; } +} diff --git a/DysonNetwork.Sphere/Auth/OidcProvider/Services/OidcProviderService.cs b/DysonNetwork.Sphere/Auth/OidcProvider/Services/OidcProviderService.cs new file mode 100644 index 0000000..7a9188c --- /dev/null +++ b/DysonNetwork.Sphere/Auth/OidcProvider/Services/OidcProviderService.cs @@ -0,0 +1,301 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using DysonNetwork.Sphere.Auth.OidcProvider.Models; +using DysonNetwork.Sphere.Auth.OidcProvider.Options; +using DysonNetwork.Sphere.Auth.OidcProvider.Responses; +using DysonNetwork.Sphere.Developer; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using NodaTime; + +namespace DysonNetwork.Sphere.Auth.OidcProvider.Services; + +public class OidcProviderService( + AppDatabase db, + IClock clock, + IOptions options, + ILogger logger +) +{ + private readonly OidcProviderOptions _options = options.Value; + + public async Task FindClientByIdAsync(Guid clientId) + { + return await db.CustomApps + .Include(c => c.Secrets) + .FirstOrDefaultAsync(c => c.Id == clientId); + } + + public async Task FindClientByAppIdAsync(Guid appId) + { + return await db.CustomApps + .Include(c => c.Secrets) + .FirstOrDefaultAsync(c => c.Id == appId); + } + + public async Task ValidateClientCredentialsAsync(Guid clientId, string clientSecret) + { + var client = await FindClientByIdAsync(clientId); + if (client == null) return false; + + var secret = client.Secrets + .Where(s => s.IsOidc && (s.ExpiredAt == null || s.ExpiredAt > clock.GetCurrentInstant())) + .FirstOrDefault(s => s.Secret == clientSecret); // In production, use proper hashing + + return secret != null; + } + + public async Task GenerateTokenResponseAsync( + Guid clientId, + string subjectId, + IEnumerable? scopes = null, + string? authorizationCode = null) + { + var client = await FindClientByIdAsync(clientId); + if (client == null) + throw new InvalidOperationException("Client not found"); + + var now = clock.GetCurrentInstant(); + var expiresIn = (int)_options.AccessTokenLifetime.TotalSeconds; + var expiresAt = now.Plus(Duration.FromSeconds(expiresIn)); + + // Generate access token + var accessToken = GenerateJwtToken(client, subjectId, expiresAt, scopes); + var refreshToken = GenerateRefreshToken(); + + // In a real implementation, you would store the token in the database + // For this example, we'll just return the token without storing it + // as we don't have a dedicated OIDC token table + + return new TokenResponse + { + AccessToken = accessToken, + ExpiresIn = expiresIn, + TokenType = "Bearer", + RefreshToken = refreshToken, + Scope = scopes != null ? string.Join(" ", scopes) : null + }; + } + + private string GenerateJwtToken(CustomApp client, string subjectId, Instant expiresAt, IEnumerable? scopes = null) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(_options.SigningKey); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(new[] + { + new Claim(JwtRegisteredClaimNames.Sub, subjectId), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, clock.GetCurrentInstant().ToUnixTimeSeconds().ToString(), + ClaimValueTypes.Integer64), + new Claim("client_id", client.Id.ToString()) + }), + Expires = expiresAt.ToDateTimeUtc(), + Issuer = _options.IssuerUri, + Audience = client.Id.ToString(), + SigningCredentials = new SigningCredentials( + new SymmetricSecurityKey(key), + SecurityAlgorithms.HmacSha256Signature) + }; + + // Add scopes as claims if provided, otherwise use client's default scopes + var effectiveScopes = scopes?.ToList() ?? client.AllowedScopes?.ToList() ?? new List(); + if (effectiveScopes.Any()) + { + tokenDescriptor.Subject.AddClaims( + effectiveScopes.Select(scope => new Claim("scope", scope))); + } + + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } + + private static string GenerateRefreshToken() + { + using var rng = RandomNumberGenerator.Create(); + var bytes = new byte[32]; + rng.GetBytes(bytes); + return Convert.ToBase64String(bytes) + .Replace("+", "-") + .Replace("/", "_") + .Replace("=", ""); + } + + private static bool VerifyHashedSecret(string secret, string hashedSecret) + { + // In a real implementation, you'd use a proper password hashing algorithm like PBKDF2, bcrypt, or Argon2 + // For now, we'll do a simple comparison, but you should replace this with proper hashing + return string.Equals(secret, hashedSecret, StringComparison.Ordinal); + } + + public JwtSecurityToken? ValidateToken(string token) + { + try + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(_options.SigningKey); + + tokenHandler.ValidateToken(token, new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = true, + ValidIssuer = _options.IssuerUri, + ValidateAudience = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }, out var validatedToken); + + return (JwtSecurityToken)validatedToken; + } + catch (Exception ex) + { + logger.LogError(ex, "Token validation failed"); + return null; + } + } + + private static readonly Dictionary _authorizationCodes = new(); + + public async Task GenerateAuthorizationCodeAsync( + Guid clientId, + string userId, + string redirectUri, + IEnumerable scopes, + string? codeChallenge = null, + string? codeChallengeMethod = null, + string? nonce = null) + { + // Generate a random code + var code = GenerateRandomString(32); + var now = clock.GetCurrentInstant(); + + // Store the code with its metadata + _authorizationCodes[code] = new AuthorizationCodeInfo + { + ClientId = clientId, + UserId = userId, + RedirectUri = redirectUri, + Scopes = scopes.ToList(), + CodeChallenge = codeChallenge, + CodeChallengeMethod = codeChallengeMethod, + Nonce = nonce, + Expiration = now.Plus(Duration.FromTimeSpan(_options.AuthorizationCodeLifetime)), + CreatedAt = now + }; + + logger.LogInformation("Generated authorization code for client {ClientId} and user {UserId}", clientId, userId); + return code; + } + + public async Task ValidateAuthorizationCodeAsync( + string code, + Guid clientId, + string? redirectUri = null, + string? codeVerifier = null) + { + if (!_authorizationCodes.TryGetValue(code, out var authCode) || authCode == null) + { + logger.LogWarning("Authorization code not found: {Code}", code); + return null; + } + + var now = clock.GetCurrentInstant(); + + // Check if code has expired + if (now > authCode.Expiration) + { + logger.LogWarning("Authorization code expired: {Code}", code); + _authorizationCodes.Remove(code); + return null; + } + + // Verify client ID matches + if (authCode.ClientId != clientId) + { + logger.LogWarning("Client ID mismatch for code {Code}. Expected: {ExpectedClientId}, Actual: {ActualClientId}", + code, authCode.ClientId, clientId); + return null; + } + + // Verify redirect URI if provided + if (!string.IsNullOrEmpty(redirectUri) && authCode.RedirectUri != redirectUri) + { + logger.LogWarning("Redirect URI mismatch for code {Code}", code); + return null; + } + + // Verify PKCE code challenge if one was provided during authorization + if (!string.IsNullOrEmpty(authCode.CodeChallenge)) + { + if (string.IsNullOrEmpty(codeVerifier)) + { + logger.LogWarning("PKCE code verifier is required but not provided for code {Code}", code); + return null; + } + + var isValid = authCode.CodeChallengeMethod?.ToUpperInvariant() switch + { + "S256" => VerifyCodeChallenge(codeVerifier, authCode.CodeChallenge, "S256"), + "PLAIN" => VerifyCodeChallenge(codeVerifier, authCode.CodeChallenge, "PLAIN"), + _ => false // Unsupported code challenge method + }; + + if (!isValid) + { + logger.LogWarning("PKCE code verifier validation failed for code {Code}", code); + return null; + } + } + + // Code is valid, remove it from the store (codes are single-use) + _authorizationCodes.Remove(code); + + return authCode; + } + + private static string GenerateRandomString(int length) + { + const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"; + var random = RandomNumberGenerator.Create(); + var result = new char[length]; + + for (int i = 0; i < length; i++) + { + var randomNumber = new byte[4]; + random.GetBytes(randomNumber); + var index = (int)(BitConverter.ToUInt32(randomNumber, 0) % chars.Length); + result[i] = chars[index]; + } + + return new string(result); + } + + private static bool VerifyCodeChallenge(string codeVerifier, string codeChallenge, string method) + { + if (string.IsNullOrEmpty(codeVerifier)) return false; + + if (method == "S256") + { + using var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier)); + var base64 = Base64UrlEncoder.Encode(hash); + return string.Equals(base64, codeChallenge, StringComparison.Ordinal); + } + + if (method == "PLAIN") + { + return string.Equals(codeVerifier, codeChallenge, StringComparison.Ordinal); + } + + return false; + } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Data/Migrations/20250628172328_AddOidcProviderSupport.Designer.cs b/DysonNetwork.Sphere/Data/Migrations/20250628172328_AddOidcProviderSupport.Designer.cs new file mode 100644 index 0000000..96facd5 --- /dev/null +++ b/DysonNetwork.Sphere/Data/Migrations/20250628172328_AddOidcProviderSupport.Designer.cs @@ -0,0 +1,4000 @@ +// +using System; +using System.Collections.Generic; +using System.Text.Json; +using DysonNetwork.Sphere; +using DysonNetwork.Sphere.Account; +using DysonNetwork.Sphere.Chat; +using DysonNetwork.Sphere.Connection.WebReader; +using DysonNetwork.Sphere.Storage; +using DysonNetwork.Sphere.Wallet; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace DysonNetwork.Sphere.Data.Migrations +{ + [DbContext(typeof(AppDatabase))] + [Migration("20250628172328_AddOidcProviderSupport")] + partial class AddOidcProviderSupport + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.AbuseReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("reason"); + + b.Property("Resolution") + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("resolution"); + + b.Property("ResolvedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("resolved_at"); + + b.Property("ResourceIdentifier") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("resource_identifier"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_abuse_reports"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_abuse_reports_account_id"); + + b.ToTable("abuse_reports", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("activated_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsSuperuser") + .HasColumnType("boolean") + .HasColumnName("is_superuser"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("language"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("Nick") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("nick"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_accounts"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_accounts_name"); + + b.ToTable("accounts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountAuthFactor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property>("Config") + .HasColumnType("jsonb") + .HasColumnName("config"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("EnabledAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("enabled_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Secret") + .HasMaxLength(8196) + .HasColumnType("character varying(8196)") + .HasColumnName("secret"); + + b.Property("Trustworthy") + .HasColumnType("integer") + .HasColumnName("trustworthy"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_auth_factors"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_auth_factors_account_id"); + + b.ToTable("account_auth_factors", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccessToken") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("access_token"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_at"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("ProvidedIdentifier") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("provided_identifier"); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("provider"); + + b.Property("RefreshToken") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("refresh_token"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_connections"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_connections_account_id"); + + b.ToTable("account_connections", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountContact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsPrimary") + .HasColumnType("boolean") + .HasColumnName("is_primary"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_account_contacts"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_contacts_account_id"); + + b.ToTable("account_contacts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.ActionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("action"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IpAddress") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ip_address"); + + b.Property("Location") + .HasColumnType("geometry") + .HasColumnName("location"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("SessionId") + .HasColumnType("uuid") + .HasColumnName("session_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("user_agent"); + + b.HasKey("Id") + .HasName("pk_action_logs"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_action_logs_account_id"); + + b.ToTable("action_logs", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Badge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("activated_at"); + + b.Property("Caption") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("caption"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Label") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_badges"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_badges_account_id"); + + b.ToTable("badges", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.CheckInResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Level") + .HasColumnType("integer") + .HasColumnName("level"); + + b.Property("RewardExperience") + .HasColumnType("integer") + .HasColumnName("reward_experience"); + + b.Property("RewardPoints") + .HasColumnType("numeric") + .HasColumnName("reward_points"); + + b.Property>("Tips") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("tips"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_check_in_results"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_check_in_results_account_id"); + + b.ToTable("account_check_in_results", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.MagicSpell", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property>("Meta") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Spell") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("spell"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_magic_spells"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_magic_spells_account_id"); + + b.HasIndex("Spell") + .IsUnique() + .HasDatabaseName("ix_magic_spells_spell"); + + b.ToTable("magic_spells", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Content") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("Subtitle") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("subtitle"); + + b.Property("Title") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("title"); + + b.Property("Topic") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("topic"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("ViewedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("viewed_at"); + + b.HasKey("Id") + .HasName("pk_notifications"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_notifications_account_id"); + + b.ToTable("notifications", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.NotificationPushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("device_id"); + + b.Property("DeviceToken") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("device_token"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_used_at"); + + b.Property("Provider") + .HasColumnType("integer") + .HasColumnName("provider"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_notification_push_subscriptions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_notification_push_subscriptions_account_id"); + + b.HasIndex("DeviceToken", "DeviceId", "AccountId") + .IsUnique() + .HasDatabaseName("ix_notification_push_subscriptions_device_token_device_id_acco"); + + b.ToTable("notification_push_subscriptions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("ActiveBadge") + .HasColumnType("jsonb") + .HasColumnName("active_badge"); + + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); + + b.Property("BackgroundId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("background_id"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("bio"); + + b.Property("Birthday") + .HasColumnType("timestamp with time zone") + .HasColumnName("birthday"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Experience") + .HasColumnType("integer") + .HasColumnName("experience"); + + b.Property("FirstName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("first_name"); + + b.Property("Gender") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("gender"); + + b.Property("LastName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("last_name"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_at"); + + b.Property("Location") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("location"); + + b.Property("MiddleName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("middle_name"); + + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); + + b.Property("PictureId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("picture_id"); + + b.Property("Pronouns") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("pronouns"); + + b.Property("StellarMembership") + .HasColumnType("jsonb") + .HasColumnName("stellar_membership"); + + b.Property("TimeZone") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("time_zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Verification") + .HasColumnType("jsonb") + .HasColumnName("verification"); + + b.HasKey("Id") + .HasName("pk_account_profiles"); + + b.HasIndex("AccountId") + .IsUnique() + .HasDatabaseName("ix_account_profiles_account_id"); + + b.ToTable("account_profiles", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Relationship", b => + { + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("RelatedId") + .HasColumnType("uuid") + .HasColumnName("related_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Status") + .HasColumnType("smallint") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("AccountId", "RelatedId") + .HasName("pk_account_relationships"); + + b.HasIndex("RelatedId") + .HasDatabaseName("ix_account_relationships_related_id"); + + b.ToTable("account_relationships", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Status", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Attitude") + .HasColumnType("integer") + .HasColumnName("attitude"); + + b.Property("ClearedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("cleared_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsInvisible") + .HasColumnType("boolean") + .HasColumnName("is_invisible"); + + b.Property("IsNotDisturb") + .HasColumnType("boolean") + .HasColumnName("is_not_disturb"); + + b.Property("Label") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_account_statuses"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_account_statuses_account_id"); + + b.ToTable("account_statuses", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Auth.Challenge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property>("Audiences") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("audiences"); + + b.Property>("BlacklistFactors") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("blacklist_factors"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeviceId") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("device_id"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("FailedAttempts") + .HasColumnType("integer") + .HasColumnName("failed_attempts"); + + b.Property("IpAddress") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("ip_address"); + + b.Property("Location") + .HasColumnType("geometry") + .HasColumnName("location"); + + b.Property("Nonce") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("nonce"); + + b.Property("Platform") + .HasColumnType("integer") + .HasColumnName("platform"); + + b.Property>("Scopes") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("scopes"); + + b.Property("StepRemain") + .HasColumnType("integer") + .HasColumnName("step_remain"); + + b.Property("StepTotal") + .HasColumnType("integer") + .HasColumnName("step_total"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("user_agent"); + + b.HasKey("Id") + .HasName("pk_auth_challenges"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_auth_challenges_account_id"); + + b.ToTable("auth_challenges", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Auth.Session", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("ChallengeId") + .HasColumnType("uuid") + .HasColumnName("challenge_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Label") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("label"); + + b.Property("LastGrantedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_granted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_auth_sessions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_auth_sessions_account_id"); + + b.HasIndex("ChallengeId") + .HasDatabaseName("ix_auth_sessions_challenge_id"); + + b.ToTable("auth_sessions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.ChatMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("BreakUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("break_until"); + + b.Property("ChatRoomId") + .HasColumnType("uuid") + .HasColumnName("chat_room_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("IsBot") + .HasColumnType("boolean") + .HasColumnName("is_bot"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_at"); + + b.Property("LastReadAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_at"); + + b.Property("LeaveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("leave_at"); + + b.Property("Nick") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("nick"); + + b.Property("Notify") + .HasColumnType("integer") + .HasColumnName("notify"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("TimeoutCause") + .HasColumnType("jsonb") + .HasColumnName("timeout_cause"); + + b.Property("TimeoutUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("timeout_until"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_chat_members"); + + b.HasAlternateKey("ChatRoomId", "AccountId") + .HasName("ak_chat_members_chat_room_id_account_id"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_chat_members_account_id"); + + b.ToTable("chat_members", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.ChatRoom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); + + b.Property("BackgroundId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("background_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("IsCommunity") + .HasColumnType("boolean") + .HasColumnName("is_community"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Name") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); + + b.Property("PictureId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("picture_id"); + + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_chat_rooms"); + + b.HasIndex("RealmId") + .HasDatabaseName("ix_chat_rooms_realm_id"); + + b.ToTable("chat_rooms", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property>("Attachments") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("attachments"); + + b.Property("ChatRoomId") + .HasColumnType("uuid") + .HasColumnName("chat_room_id"); + + b.Property("Content") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("EditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("edited_at"); + + b.Property("ForwardedMessageId") + .HasColumnType("uuid") + .HasColumnName("forwarded_message_id"); + + b.Property>("MembersMentioned") + .HasColumnType("jsonb") + .HasColumnName("members_mentioned"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Nonce") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("character varying(36)") + .HasColumnName("nonce"); + + b.Property("RepliedMessageId") + .HasColumnType("uuid") + .HasColumnName("replied_message_id"); + + b.Property("SenderId") + .HasColumnType("uuid") + .HasColumnName("sender_id"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_chat_messages"); + + b.HasIndex("ChatRoomId") + .HasDatabaseName("ix_chat_messages_chat_room_id"); + + b.HasIndex("ForwardedMessageId") + .HasDatabaseName("ix_chat_messages_forwarded_message_id"); + + b.HasIndex("RepliedMessageId") + .HasDatabaseName("ix_chat_messages_replied_message_id"); + + b.HasIndex("SenderId") + .HasDatabaseName("ix_chat_messages_sender_id"); + + b.ToTable("chat_messages", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.MessageReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Attitude") + .HasColumnType("integer") + .HasColumnName("attitude"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("MessageId") + .HasColumnType("uuid") + .HasColumnName("message_id"); + + b.Property("SenderId") + .HasColumnType("uuid") + .HasColumnName("sender_id"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("symbol"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_chat_reactions"); + + b.HasIndex("MessageId") + .HasDatabaseName("ix_chat_reactions_message_id"); + + b.HasIndex("SenderId") + .HasDatabaseName("ix_chat_reactions_sender_id"); + + b.ToTable("chat_reactions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.RealtimeCall", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("ProviderName") + .HasColumnType("text") + .HasColumnName("provider_name"); + + b.Property("RoomId") + .HasColumnType("uuid") + .HasColumnName("room_id"); + + b.Property("SenderId") + .HasColumnType("uuid") + .HasColumnName("sender_id"); + + b.Property("SessionId") + .HasColumnType("text") + .HasColumnName("session_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpstreamConfigJson") + .HasColumnType("jsonb") + .HasColumnName("upstream"); + + b.HasKey("Id") + .HasName("pk_chat_realtime_call"); + + b.HasIndex("RoomId") + .HasDatabaseName("ix_chat_realtime_call_room_id"); + + b.HasIndex("SenderId") + .HasDatabaseName("ix_chat_realtime_call_sender_id"); + + b.ToTable("chat_realtime_call", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Connection.WebReader.WebArticle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Author") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("author"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("FeedId") + .HasColumnType("uuid") + .HasColumnName("feed_id"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("Preview") + .HasColumnType("jsonb") + .HasColumnName("preview"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("pk_web_articles"); + + b.HasIndex("FeedId") + .HasDatabaseName("ix_web_articles_feed_id"); + + b.HasIndex("Url") + .IsUnique() + .HasDatabaseName("ix_web_articles_url"); + + b.ToTable("web_articles", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Connection.WebReader.WebFeed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Config") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("config"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("description"); + + b.Property("Preview") + .HasColumnType("jsonb") + .HasColumnName("preview"); + + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(8192) + .HasColumnType("character varying(8192)") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("pk_web_feeds"); + + b.HasIndex("PublisherId") + .HasDatabaseName("ix_web_feeds_publisher_id"); + + b.HasIndex("Url") + .IsUnique() + .HasDatabaseName("ix_web_feeds_url"); + + b.ToTable("web_feeds", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Developer.CustomApp", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AllowOfflineAccess") + .HasColumnType("boolean") + .HasColumnName("allow_offline_access"); + + b.Property("AllowedGrantTypes") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("allowed_grant_types"); + + b.Property("AllowedScopes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("allowed_scopes"); + + b.Property("ClientUri") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("client_uri"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("LogoUri") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("logo_uri"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property("PostLogoutRedirectUris") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("post_logout_redirect_uris"); + + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("RedirectUris") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("redirect_uris"); + + b.Property("RequirePkce") + .HasColumnType("boolean") + .HasColumnName("require_pkce"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("slug"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("VerifiedAs") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("verified_as"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_custom_apps"); + + b.HasIndex("PublisherId") + .HasDatabaseName("ix_custom_apps_publisher_id"); + + b.ToTable("custom_apps", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Developer.CustomAppSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AppId") + .HasColumnType("uuid") + .HasColumnName("app_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("IsOidc") + .HasColumnType("boolean") + .HasColumnName("is_oidc"); + + b.Property("Secret") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("secret"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_custom_app_secrets"); + + b.HasIndex("AppId") + .HasDatabaseName("ix_custom_app_secrets_app_id"); + + b.HasIndex("Secret") + .IsUnique() + .HasDatabaseName("ix_custom_app_secrets_secret"); + + b.ToTable("custom_app_secrets", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("key"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_permission_groups"); + + b.ToTable("permission_groups", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroupMember", b => + { + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("Actor") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("actor"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("GroupId", "Actor") + .HasName("pk_permission_group_members"); + + b.ToTable("permission_group_members", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionNode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Actor") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("actor"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("Area") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("area"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("GroupId") + .HasColumnType("uuid") + .HasColumnName("group_id"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("key"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Value") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_permission_nodes"); + + b.HasIndex("GroupId") + .HasDatabaseName("ix_permission_nodes_group_id"); + + b.HasIndex("Key", "Area", "Actor") + .HasDatabaseName("ix_permission_nodes_key_area_actor"); + + b.ToTable("permission_nodes", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property>("Attachments") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("attachments"); + + b.Property("Content") + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("Downvotes") + .HasColumnType("integer") + .HasColumnName("downvotes"); + + b.Property("EditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("edited_at"); + + b.Property("ForwardedPostId") + .HasColumnType("uuid") + .HasColumnName("forwarded_post_id"); + + b.Property("Language") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("language"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("published_at"); + + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("RepliedPostId") + .HasColumnType("uuid") + .HasColumnName("replied_post_id"); + + b.Property("SearchVector") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("tsvector") + .HasColumnName("search_vector") + .HasAnnotation("Npgsql:TsVectorConfig", "simple") + .HasAnnotation("Npgsql:TsVectorProperties", new[] { "Title", "Description", "Content" }); + + b.Property>("SensitiveMarks") + .HasColumnType("jsonb") + .HasColumnName("sensitive_marks"); + + b.Property("Title") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("title"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Upvotes") + .HasColumnType("integer") + .HasColumnName("upvotes"); + + b.Property("ViewsTotal") + .HasColumnType("integer") + .HasColumnName("views_total"); + + b.Property("ViewsUnique") + .HasColumnType("integer") + .HasColumnName("views_unique"); + + b.Property("Visibility") + .HasColumnType("integer") + .HasColumnName("visibility"); + + b.HasKey("Id") + .HasName("pk_posts"); + + b.HasIndex("ForwardedPostId") + .HasDatabaseName("ix_posts_forwarded_post_id"); + + b.HasIndex("PublisherId") + .HasDatabaseName("ix_posts_publisher_id"); + + b.HasIndex("RepliedPostId") + .HasDatabaseName("ix_posts_replied_post_id"); + + b.HasIndex("SearchVector") + .HasDatabaseName("ix_posts_search_vector"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchVector"), "GIN"); + + b.ToTable("posts", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.PostCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("slug"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_post_categories"); + + b.ToTable("post_categories", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.PostCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("slug"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_post_collections"); + + b.HasIndex("PublisherId") + .HasDatabaseName("ix_post_collections_publisher_id"); + + b.ToTable("post_collections", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.PostReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Attitude") + .HasColumnType("integer") + .HasColumnName("attitude"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("PostId") + .HasColumnType("uuid") + .HasColumnName("post_id"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("symbol"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_post_reactions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_post_reactions_account_id"); + + b.HasIndex("PostId") + .HasDatabaseName("ix_post_reactions_post_id"); + + b.ToTable("post_reactions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.PostTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("slug"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_post_tags"); + + b.ToTable("post_tags", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.Publisher", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); + + b.Property("BackgroundId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("background_id"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("bio"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("Nick") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("nick"); + + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); + + b.Property("PictureId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("picture_id"); + + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Verification") + .HasColumnType("jsonb") + .HasColumnName("verification"); + + b.HasKey("Id") + .HasName("pk_publishers"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_publishers_account_id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_publishers_name"); + + b.HasIndex("RealmId") + .HasDatabaseName("ix_publishers_realm_id"); + + b.ToTable("publishers", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.PublisherFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Flag") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("flag"); + + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_publisher_features"); + + b.HasIndex("PublisherId") + .HasDatabaseName("ix_publisher_features_publisher_id"); + + b.ToTable("publisher_features", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.PublisherMember", b => + { + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_at"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("PublisherId", "AccountId") + .HasName("pk_publisher_members"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_publisher_members_account_id"); + + b.ToTable("publisher_members", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.PublisherSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("Tier") + .HasColumnType("integer") + .HasColumnName("tier"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_publisher_subscriptions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_publisher_subscriptions_account_id"); + + b.HasIndex("PublisherId") + .HasDatabaseName("ix_publisher_subscriptions_publisher_id"); + + b.ToTable("publisher_subscriptions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Realm.Realm", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("Background") + .HasColumnType("jsonb") + .HasColumnName("background"); + + b.Property("BackgroundId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("background_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("IsCommunity") + .HasColumnType("boolean") + .HasColumnName("is_community"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property("Picture") + .HasColumnType("jsonb") + .HasColumnName("picture"); + + b.Property("PictureId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("picture_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("slug"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Verification") + .HasColumnType("jsonb") + .HasColumnName("verification"); + + b.HasKey("Id") + .HasName("pk_realms"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_realms_account_id"); + + b.HasIndex("Slug") + .IsUnique() + .HasDatabaseName("ix_realms_slug"); + + b.ToTable("realms", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Realm.RealmMember", b => + { + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("joined_at"); + + b.Property("LeaveAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("leave_at"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("RealmId", "AccountId") + .HasName("pk_realm_members"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_realm_members_account_id"); + + b.ToTable("realm_members", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Realm.RealmTag", b => + { + b.Property("RealmId") + .HasColumnType("uuid") + .HasColumnName("realm_id"); + + b.Property("TagId") + .HasColumnType("uuid") + .HasColumnName("tag_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("RealmId", "TagId") + .HasName("pk_realm_tags"); + + b.HasIndex("TagId") + .HasDatabaseName("ix_realm_tags_tag_id"); + + b.ToTable("realm_tags", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Realm.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_tags"); + + b.ToTable("tags", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Sticker.Sticker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Image") + .HasColumnType("jsonb") + .HasColumnName("image"); + + b.Property("ImageId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("image_id"); + + b.Property("PackId") + .HasColumnType("uuid") + .HasColumnName("pack_id"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("slug"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_stickers"); + + b.HasIndex("PackId") + .HasDatabaseName("ix_stickers_pack_id"); + + b.HasIndex("Slug") + .HasDatabaseName("ix_stickers_slug"); + + b.ToTable("stickers", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Sticker.StickerPack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property("Prefix") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("prefix"); + + b.Property("PublisherId") + .HasColumnType("uuid") + .HasColumnName("publisher_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_sticker_packs"); + + b.HasIndex("Prefix") + .IsUnique() + .HasDatabaseName("ix_sticker_packs_prefix"); + + b.HasIndex("PublisherId") + .HasDatabaseName("ix_sticker_packs_publisher_id"); + + b.ToTable("sticker_packs", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b => + { + b.Property("Id") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + + b.Property>("FileMeta") + .HasColumnType("jsonb") + .HasColumnName("file_meta"); + + b.Property("HasCompression") + .HasColumnType("boolean") + .HasColumnName("has_compression"); + + b.Property("Hash") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("hash"); + + b.Property("IsMarkedRecycle") + .HasColumnType("boolean") + .HasColumnName("is_marked_recycle"); + + b.Property("MessageId") + .HasColumnType("uuid") + .HasColumnName("message_id"); + + b.Property("MimeType") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("mime_type"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("name"); + + b.Property("PostId") + .HasColumnType("uuid") + .HasColumnName("post_id"); + + b.Property>("SensitiveMarks") + .HasColumnType("jsonb") + .HasColumnName("sensitive_marks"); + + b.Property("Size") + .HasColumnType("bigint") + .HasColumnName("size"); + + b.Property("StorageId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("storage_id"); + + b.Property("StorageUrl") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("storage_url"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("uploaded_at"); + + b.Property("UploadedTo") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("uploaded_to"); + + b.Property>("UserMeta") + .HasColumnType("jsonb") + .HasColumnName("user_meta"); + + b.HasKey("Id") + .HasName("pk_files"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_files_account_id"); + + b.HasIndex("MessageId") + .HasDatabaseName("ix_files_message_id"); + + b.HasIndex("PostId") + .HasDatabaseName("ix_files_post_id"); + + b.ToTable("files", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFileReference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("FileId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("file_id"); + + b.Property("ResourceId") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("resource_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Usage") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("usage"); + + b.HasKey("Id") + .HasName("pk_file_references"); + + b.HasIndex("FileId") + .HasDatabaseName("ix_file_references_file_id"); + + b.ToTable("file_references", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AffectedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("affected_at"); + + b.Property("Code") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("code"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DiscountAmount") + .HasColumnType("numeric") + .HasColumnName("discount_amount"); + + b.Property("DiscountRate") + .HasColumnType("double precision") + .HasColumnName("discount_rate"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("Identifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("identifier"); + + b.Property("MaxUsage") + .HasColumnType("integer") + .HasColumnName("max_usage"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_coupons"); + + b.ToTable("wallet_coupons", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("AppIdentifier") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("app_identifier"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expired_at"); + + b.Property("IssuerAppId") + .HasColumnType("uuid") + .HasColumnName("issuer_app_id"); + + b.Property>("Meta") + .HasColumnType("jsonb") + .HasColumnName("meta"); + + b.Property("PayeeWalletId") + .HasColumnType("uuid") + .HasColumnName("payee_wallet_id"); + + b.Property("Remarks") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("remarks"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("TransactionId") + .HasColumnType("uuid") + .HasColumnName("transaction_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_payment_orders"); + + b.HasIndex("IssuerAppId") + .HasDatabaseName("ix_payment_orders_issuer_app_id"); + + b.HasIndex("PayeeWalletId") + .HasDatabaseName("ix_payment_orders_payee_wallet_id"); + + b.HasIndex("TransactionId") + .HasDatabaseName("ix_payment_orders_transaction_id"); + + b.ToTable("payment_orders", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("BasePrice") + .HasColumnType("numeric") + .HasColumnName("base_price"); + + b.Property("BegunAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("begun_at"); + + b.Property("CouponId") + .HasColumnType("uuid") + .HasColumnName("coupon_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("ended_at"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("identifier"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("IsFreeTrial") + .HasColumnType("boolean") + .HasColumnName("is_free_trial"); + + b.Property("PaymentDetails") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payment_details"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("payment_method"); + + b.Property("RenewalAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("renewal_at"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallet_subscriptions"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_wallet_subscriptions_account_id"); + + b.HasIndex("CouponId") + .HasDatabaseName("ix_wallet_subscriptions_coupon_id"); + + b.HasIndex("Identifier") + .HasDatabaseName("ix_wallet_subscriptions_identifier"); + + b.ToTable("wallet_subscriptions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("PayeeWalletId") + .HasColumnType("uuid") + .HasColumnName("payee_wallet_id"); + + b.Property("PayerWalletId") + .HasColumnType("uuid") + .HasColumnName("payer_wallet_id"); + + b.Property("Remarks") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("remarks"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_payment_transactions"); + + b.HasIndex("PayeeWalletId") + .HasDatabaseName("ix_payment_transactions_payee_wallet_id"); + + b.HasIndex("PayerWalletId") + .HasDatabaseName("ix_payment_transactions_payer_wallet_id"); + + b.ToTable("payment_transactions", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Wallet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AccountId") + .HasColumnType("uuid") + .HasColumnName("account_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_wallets"); + + b.HasIndex("AccountId") + .HasDatabaseName("ix_wallets_account_id"); + + b.ToTable("wallets", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.WalletPocket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("numeric") + .HasColumnName("amount"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("currency"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("WalletId") + .HasColumnType("uuid") + .HasColumnName("wallet_id"); + + b.HasKey("Id") + .HasName("pk_wallet_pockets"); + + b.HasIndex("WalletId") + .HasDatabaseName("ix_wallet_pockets_wallet_id"); + + b.ToTable("wallet_pockets", (string)null); + }); + + modelBuilder.Entity("PostPostCategory", b => + { + b.Property("CategoriesId") + .HasColumnType("uuid") + .HasColumnName("categories_id"); + + b.Property("PostsId") + .HasColumnType("uuid") + .HasColumnName("posts_id"); + + b.HasKey("CategoriesId", "PostsId") + .HasName("pk_post_category_links"); + + b.HasIndex("PostsId") + .HasDatabaseName("ix_post_category_links_posts_id"); + + b.ToTable("post_category_links", (string)null); + }); + + modelBuilder.Entity("PostPostCollection", b => + { + b.Property("CollectionsId") + .HasColumnType("uuid") + .HasColumnName("collections_id"); + + b.Property("PostsId") + .HasColumnType("uuid") + .HasColumnName("posts_id"); + + b.HasKey("CollectionsId", "PostsId") + .HasName("pk_post_collection_links"); + + b.HasIndex("PostsId") + .HasDatabaseName("ix_post_collection_links_posts_id"); + + b.ToTable("post_collection_links", (string)null); + }); + + modelBuilder.Entity("PostPostTag", b => + { + b.Property("PostsId") + .HasColumnType("uuid") + .HasColumnName("posts_id"); + + b.Property("TagsId") + .HasColumnType("uuid") + .HasColumnName("tags_id"); + + b.HasKey("PostsId", "TagsId") + .HasName("pk_post_tag_links"); + + b.HasIndex("TagsId") + .HasDatabaseName("ix_post_tag_links_tags_id"); + + b.ToTable("post_tag_links", (string)null); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.AbuseReport", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_abuse_reports_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountAuthFactor", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany("AuthFactors") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_auth_factors_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountConnection", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany("Connections") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_connections_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountContact", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany("Contacts") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_contacts_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.ActionLog", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_action_logs_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Badge", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany("Badges") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_badges_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.CheckInResult", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_check_in_results_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.MagicSpell", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .HasConstraintName("fk_magic_spells_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Notification", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_notifications_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.NotificationPushSubscription", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_notification_push_subscriptions_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Profile", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithOne("Profile") + .HasForeignKey("DysonNetwork.Sphere.Account.Profile", "AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_profiles_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Relationship", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany("OutgoingRelationships") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_relationships_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Account.Account", "Related") + .WithMany("IncomingRelationships") + .HasForeignKey("RelatedId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_relationships_accounts_related_id"); + + b.Navigation("Account"); + + b.Navigation("Related"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Status", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_account_statuses_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Auth.Challenge", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany("Challenges") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_challenges_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Auth.Session", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany("Sessions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_sessions_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Auth.Challenge", "Challenge") + .WithMany() + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_sessions_auth_challenges_challenge_id"); + + b.Navigation("Account"); + + b.Navigation("Challenge"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.ChatMember", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_members_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Chat.ChatRoom", "ChatRoom") + .WithMany("Members") + .HasForeignKey("ChatRoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_members_chat_rooms_chat_room_id"); + + b.Navigation("Account"); + + b.Navigation("ChatRoom"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.ChatRoom", b => + { + b.HasOne("DysonNetwork.Sphere.Realm.Realm", "Realm") + .WithMany("ChatRooms") + .HasForeignKey("RealmId") + .HasConstraintName("fk_chat_rooms_realms_realm_id"); + + b.Navigation("Realm"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.Message", b => + { + b.HasOne("DysonNetwork.Sphere.Chat.ChatRoom", "ChatRoom") + .WithMany() + .HasForeignKey("ChatRoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_messages_chat_rooms_chat_room_id"); + + b.HasOne("DysonNetwork.Sphere.Chat.Message", "ForwardedMessage") + .WithMany() + .HasForeignKey("ForwardedMessageId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_chat_messages_chat_messages_forwarded_message_id"); + + b.HasOne("DysonNetwork.Sphere.Chat.Message", "RepliedMessage") + .WithMany() + .HasForeignKey("RepliedMessageId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_chat_messages_chat_messages_replied_message_id"); + + b.HasOne("DysonNetwork.Sphere.Chat.ChatMember", "Sender") + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_messages_chat_members_sender_id"); + + b.Navigation("ChatRoom"); + + b.Navigation("ForwardedMessage"); + + b.Navigation("RepliedMessage"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.MessageReaction", b => + { + b.HasOne("DysonNetwork.Sphere.Chat.Message", "Message") + .WithMany("Reactions") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_reactions_chat_messages_message_id"); + + b.HasOne("DysonNetwork.Sphere.Chat.ChatMember", "Sender") + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_reactions_chat_members_sender_id"); + + b.Navigation("Message"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.RealtimeCall", b => + { + b.HasOne("DysonNetwork.Sphere.Chat.ChatRoom", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_realtime_call_chat_rooms_room_id"); + + b.HasOne("DysonNetwork.Sphere.Chat.ChatMember", "Sender") + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_chat_realtime_call_chat_members_sender_id"); + + b.Navigation("Room"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Connection.WebReader.WebArticle", b => + { + b.HasOne("DysonNetwork.Sphere.Connection.WebReader.WebFeed", "Feed") + .WithMany("Articles") + .HasForeignKey("FeedId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_web_articles_web_feeds_feed_id"); + + b.Navigation("Feed"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Connection.WebReader.WebFeed", b => + { + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher") + .WithMany() + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_web_feeds_publishers_publisher_id"); + + b.Navigation("Publisher"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Developer.CustomApp", b => + { + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Developer") + .WithMany() + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_custom_apps_publishers_publisher_id"); + + b.Navigation("Developer"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Developer.CustomAppSecret", b => + { + b.HasOne("DysonNetwork.Sphere.Developer.CustomApp", "App") + .WithMany("Secrets") + .HasForeignKey("AppId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_custom_app_secrets_custom_apps_app_id"); + + b.Navigation("App"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroupMember", b => + { + b.HasOne("DysonNetwork.Sphere.Permission.PermissionGroup", "Group") + .WithMany("Members") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_permission_group_members_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionNode", b => + { + b.HasOne("DysonNetwork.Sphere.Permission.PermissionGroup", "Group") + .WithMany("Nodes") + .HasForeignKey("GroupId") + .HasConstraintName("fk_permission_nodes_permission_groups_group_id"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.Post", b => + { + b.HasOne("DysonNetwork.Sphere.Post.Post", "ForwardedPost") + .WithMany() + .HasForeignKey("ForwardedPostId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_posts_posts_forwarded_post_id"); + + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher") + .WithMany("Posts") + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_posts_publishers_publisher_id"); + + b.HasOne("DysonNetwork.Sphere.Post.Post", "RepliedPost") + .WithMany() + .HasForeignKey("RepliedPostId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_posts_posts_replied_post_id"); + + b.Navigation("ForwardedPost"); + + b.Navigation("Publisher"); + + b.Navigation("RepliedPost"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.PostCollection", b => + { + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher") + .WithMany("Collections") + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_collections_publishers_publisher_id"); + + b.Navigation("Publisher"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.PostReaction", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_reactions_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Post.Post", "Post") + .WithMany("Reactions") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_reactions_posts_post_id"); + + b.Navigation("Account"); + + b.Navigation("Post"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.Publisher", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .HasConstraintName("fk_publishers_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Realm.Realm", "Realm") + .WithMany() + .HasForeignKey("RealmId") + .HasConstraintName("fk_publishers_realms_realm_id"); + + b.Navigation("Account"); + + b.Navigation("Realm"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.PublisherFeature", b => + { + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher") + .WithMany() + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_publisher_features_publishers_publisher_id"); + + b.Navigation("Publisher"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.PublisherMember", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_publisher_members_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher") + .WithMany("Members") + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_publisher_members_publishers_publisher_id"); + + b.Navigation("Account"); + + b.Navigation("Publisher"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.PublisherSubscription", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_publisher_subscriptions_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher") + .WithMany("Subscriptions") + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_publisher_subscriptions_publishers_publisher_id"); + + b.Navigation("Account"); + + b.Navigation("Publisher"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Realm.Realm", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_realms_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Realm.RealmMember", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_realm_members_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Realm.Realm", "Realm") + .WithMany("Members") + .HasForeignKey("RealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_realm_members_realms_realm_id"); + + b.Navigation("Account"); + + b.Navigation("Realm"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Realm.RealmTag", b => + { + b.HasOne("DysonNetwork.Sphere.Realm.Realm", "Realm") + .WithMany("RealmTags") + .HasForeignKey("RealmId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_realm_tags_realms_realm_id"); + + b.HasOne("DysonNetwork.Sphere.Realm.Tag", "Tag") + .WithMany("RealmTags") + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_realm_tags_tags_tag_id"); + + b.Navigation("Realm"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Sticker.Sticker", b => + { + b.HasOne("DysonNetwork.Sphere.Sticker.StickerPack", "Pack") + .WithMany() + .HasForeignKey("PackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_stickers_sticker_packs_pack_id"); + + b.Navigation("Pack"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Sticker.StickerPack", b => + { + b.HasOne("DysonNetwork.Sphere.Publisher.Publisher", "Publisher") + .WithMany() + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_sticker_packs_publishers_publisher_id"); + + b.Navigation("Publisher"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFile", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_files_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Chat.Message", null) + .WithMany("OutdatedAttachments") + .HasForeignKey("MessageId") + .HasConstraintName("fk_files_chat_messages_message_id"); + + b.HasOne("DysonNetwork.Sphere.Post.Post", null) + .WithMany("OutdatedAttachments") + .HasForeignKey("PostId") + .HasConstraintName("fk_files_posts_post_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Storage.CloudFileReference", b => + { + b.HasOne("DysonNetwork.Sphere.Storage.CloudFile", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_file_references_files_file_id"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Order", b => + { + b.HasOne("DysonNetwork.Sphere.Developer.CustomApp", "IssuerApp") + .WithMany() + .HasForeignKey("IssuerAppId") + .HasConstraintName("fk_payment_orders_custom_apps_issuer_app_id"); + + b.HasOne("DysonNetwork.Sphere.Wallet.Wallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_orders_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Sphere.Wallet.Transaction", "Transaction") + .WithMany() + .HasForeignKey("TransactionId") + .HasConstraintName("fk_payment_orders_payment_transactions_transaction_id"); + + b.Navigation("IssuerApp"); + + b.Navigation("PayeeWallet"); + + b.Navigation("Transaction"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Subscription", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany("Subscriptions") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_subscriptions_accounts_account_id"); + + b.HasOne("DysonNetwork.Sphere.Wallet.Coupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .HasConstraintName("fk_wallet_subscriptions_wallet_coupons_coupon_id"); + + b.Navigation("Account"); + + b.Navigation("Coupon"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Transaction", b => + { + b.HasOne("DysonNetwork.Sphere.Wallet.Wallet", "PayeeWallet") + .WithMany() + .HasForeignKey("PayeeWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payee_wallet_id"); + + b.HasOne("DysonNetwork.Sphere.Wallet.Wallet", "PayerWallet") + .WithMany() + .HasForeignKey("PayerWalletId") + .HasConstraintName("fk_payment_transactions_wallets_payer_wallet_id"); + + b.Navigation("PayeeWallet"); + + b.Navigation("PayerWallet"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Wallet", b => + { + b.HasOne("DysonNetwork.Sphere.Account.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallets_accounts_account_id"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.WalletPocket", b => + { + b.HasOne("DysonNetwork.Sphere.Wallet.Wallet", "Wallet") + .WithMany("Pockets") + .HasForeignKey("WalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_wallet_pockets_wallets_wallet_id"); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("PostPostCategory", b => + { + b.HasOne("DysonNetwork.Sphere.Post.PostCategory", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_category_links_post_categories_categories_id"); + + b.HasOne("DysonNetwork.Sphere.Post.Post", null) + .WithMany() + .HasForeignKey("PostsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_category_links_posts_posts_id"); + }); + + modelBuilder.Entity("PostPostCollection", b => + { + b.HasOne("DysonNetwork.Sphere.Post.PostCollection", null) + .WithMany() + .HasForeignKey("CollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_collection_links_post_collections_collections_id"); + + b.HasOne("DysonNetwork.Sphere.Post.Post", null) + .WithMany() + .HasForeignKey("PostsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_collection_links_posts_posts_id"); + }); + + modelBuilder.Entity("PostPostTag", b => + { + b.HasOne("DysonNetwork.Sphere.Post.Post", null) + .WithMany() + .HasForeignKey("PostsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_tag_links_posts_posts_id"); + + b.HasOne("DysonNetwork.Sphere.Post.PostTag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_post_tag_links_post_tags_tags_id"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Account.Account", b => + { + b.Navigation("AuthFactors"); + + b.Navigation("Badges"); + + b.Navigation("Challenges"); + + b.Navigation("Connections"); + + b.Navigation("Contacts"); + + b.Navigation("IncomingRelationships"); + + b.Navigation("OutgoingRelationships"); + + b.Navigation("Profile") + .IsRequired(); + + b.Navigation("Sessions"); + + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.ChatRoom", b => + { + b.Navigation("Members"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Chat.Message", b => + { + b.Navigation("OutdatedAttachments"); + + b.Navigation("Reactions"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Connection.WebReader.WebFeed", b => + { + b.Navigation("Articles"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Developer.CustomApp", b => + { + b.Navigation("Secrets"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroup", b => + { + b.Navigation("Members"); + + b.Navigation("Nodes"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Post.Post", b => + { + b.Navigation("OutdatedAttachments"); + + b.Navigation("Reactions"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Publisher.Publisher", b => + { + b.Navigation("Collections"); + + b.Navigation("Members"); + + b.Navigation("Posts"); + + b.Navigation("Subscriptions"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Realm.Realm", b => + { + b.Navigation("ChatRooms"); + + b.Navigation("Members"); + + b.Navigation("RealmTags"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Realm.Tag", b => + { + b.Navigation("RealmTags"); + }); + + modelBuilder.Entity("DysonNetwork.Sphere.Wallet.Wallet", b => + { + b.Navigation("Pockets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DysonNetwork.Sphere/Data/Migrations/20250628172328_AddOidcProviderSupport.cs b/DysonNetwork.Sphere/Data/Migrations/20250628172328_AddOidcProviderSupport.cs new file mode 100644 index 0000000..2d3ae2a --- /dev/null +++ b/DysonNetwork.Sphere/Data/Migrations/20250628172328_AddOidcProviderSupport.cs @@ -0,0 +1,139 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DysonNetwork.Sphere.Data.Migrations +{ + /// + public partial class AddOidcProviderSupport : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "remarks", + table: "custom_app_secrets", + newName: "description"); + + migrationBuilder.AddColumn( + name: "allow_offline_access", + table: "custom_apps", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "allowed_grant_types", + table: "custom_apps", + type: "character varying(256)", + maxLength: 256, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "allowed_scopes", + table: "custom_apps", + type: "character varying(256)", + maxLength: 256, + nullable: true); + + migrationBuilder.AddColumn( + name: "client_uri", + table: "custom_apps", + type: "character varying(1024)", + maxLength: 1024, + nullable: true); + + migrationBuilder.AddColumn( + name: "logo_uri", + table: "custom_apps", + type: "character varying(4096)", + maxLength: 4096, + nullable: true); + + migrationBuilder.AddColumn( + name: "post_logout_redirect_uris", + table: "custom_apps", + type: "character varying(4096)", + maxLength: 4096, + nullable: true); + + migrationBuilder.AddColumn( + name: "redirect_uris", + table: "custom_apps", + type: "character varying(4096)", + maxLength: 4096, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "require_pkce", + table: "custom_apps", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "is_oidc", + table: "custom_app_secrets", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateIndex( + name: "ix_custom_app_secrets_secret", + table: "custom_app_secrets", + column: "secret", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_custom_app_secrets_secret", + table: "custom_app_secrets"); + + migrationBuilder.DropColumn( + name: "allow_offline_access", + table: "custom_apps"); + + migrationBuilder.DropColumn( + name: "allowed_grant_types", + table: "custom_apps"); + + migrationBuilder.DropColumn( + name: "allowed_scopes", + table: "custom_apps"); + + migrationBuilder.DropColumn( + name: "client_uri", + table: "custom_apps"); + + migrationBuilder.DropColumn( + name: "logo_uri", + table: "custom_apps"); + + migrationBuilder.DropColumn( + name: "post_logout_redirect_uris", + table: "custom_apps"); + + migrationBuilder.DropColumn( + name: "redirect_uris", + table: "custom_apps"); + + migrationBuilder.DropColumn( + name: "require_pkce", + table: "custom_apps"); + + migrationBuilder.DropColumn( + name: "is_oidc", + table: "custom_app_secrets"); + + migrationBuilder.RenameColumn( + name: "description", + table: "custom_app_secrets", + newName: "remarks"); + } + } +} diff --git a/DysonNetwork.Sphere/Developer/CustomApp.cs b/DysonNetwork.Sphere/Developer/CustomApp.cs index 939de6a..50b320b 100644 --- a/DysonNetwork.Sphere/Developer/CustomApp.cs +++ b/DysonNetwork.Sphere/Developer/CustomApp.cs @@ -21,7 +21,17 @@ public class CustomApp : ModelBase public Instant? VerifiedAt { get; set; } [MaxLength(4096)] public string? VerifiedAs { get; set; } - [JsonIgnore] private ICollection Secrets { get; set; } = new List(); + // OIDC/OAuth specific properties + [MaxLength(4096)] public string? LogoUri { get; set; } + [MaxLength(1024)] public string? ClientUri { get; set; } + [MaxLength(4096)] public string[] RedirectUris { get; set; } = []; + [MaxLength(4096)] public string[]? PostLogoutRedirectUris { get; set; } + [MaxLength(256)] public string[]? AllowedScopes { get; set; } = ["openid", "profile", "email"]; + [MaxLength(256)] public string[] AllowedGrantTypes { get; set; } = ["authorization_code", "refresh_token"]; + public bool RequirePkce { get; set; } = true; + public bool AllowOfflineAccess { get; set; } = false; + + [JsonIgnore] public ICollection Secrets { get; set; } = new List(); public Guid PublisherId { get; set; } public Publisher.Publisher Developer { get; set; } = null!; @@ -31,8 +41,9 @@ public class CustomAppSecret : ModelBase { public Guid Id { get; set; } = Guid.NewGuid(); [MaxLength(1024)] public string Secret { get; set; } = null!; - [MaxLength(4096)] public string? Remarks { get; set; } = null!; + [MaxLength(4096)] public string? Description { get; set; } = null!; public Instant? ExpiredAt { get; set; } + public bool IsOidc { get; set; } = false; // Indicates if this secret is for OIDC/OAuth public Guid AppId { get; set; } public CustomApp App { get; set; } = null!; diff --git a/DysonNetwork.Sphere/Migrations/AppDatabaseModelSnapshot.cs b/DysonNetwork.Sphere/Migrations/AppDatabaseModelSnapshot.cs index 3c4aeaa..d23e949 100644 --- a/DysonNetwork.Sphere/Migrations/AppDatabaseModelSnapshot.cs +++ b/DysonNetwork.Sphere/Migrations/AppDatabaseModelSnapshot.cs @@ -1500,6 +1500,26 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("uuid") .HasColumnName("id"); + b.Property("AllowOfflineAccess") + .HasColumnType("boolean") + .HasColumnName("allow_offline_access"); + + b.Property("AllowedGrantTypes") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("allowed_grant_types"); + + b.Property("AllowedScopes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("allowed_scopes"); + + b.Property("ClientUri") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("client_uri"); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasColumnName("created_at"); @@ -1508,16 +1528,36 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("timestamp with time zone") .HasColumnName("deleted_at"); + b.Property("LogoUri") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("logo_uri"); + b.Property("Name") .IsRequired() .HasMaxLength(1024) .HasColumnType("character varying(1024)") .HasColumnName("name"); + b.Property("PostLogoutRedirectUris") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("post_logout_redirect_uris"); + b.Property("PublisherId") .HasColumnType("uuid") .HasColumnName("publisher_id"); + b.Property("RedirectUris") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("redirect_uris"); + + b.Property("RequirePkce") + .HasColumnType("boolean") + .HasColumnName("require_pkce"); + b.Property("Slug") .IsRequired() .HasMaxLength(1024) @@ -1569,14 +1609,18 @@ namespace DysonNetwork.Sphere.Migrations .HasColumnType("timestamp with time zone") .HasColumnName("deleted_at"); + b.Property("Description") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("description"); + b.Property("ExpiredAt") .HasColumnType("timestamp with time zone") .HasColumnName("expired_at"); - b.Property("Remarks") - .HasMaxLength(4096) - .HasColumnType("character varying(4096)") - .HasColumnName("remarks"); + b.Property("IsOidc") + .HasColumnType("boolean") + .HasColumnName("is_oidc"); b.Property("Secret") .IsRequired() @@ -1594,6 +1638,10 @@ namespace DysonNetwork.Sphere.Migrations b.HasIndex("AppId") .HasDatabaseName("ix_custom_app_secrets_app_id"); + b.HasIndex("Secret") + .IsUnique() + .HasDatabaseName("ix_custom_app_secrets_secret"); + b.ToTable("custom_app_secrets", (string)null); }); @@ -3444,7 +3492,7 @@ namespace DysonNetwork.Sphere.Migrations modelBuilder.Entity("DysonNetwork.Sphere.Developer.CustomAppSecret", b => { b.HasOne("DysonNetwork.Sphere.Developer.CustomApp", "App") - .WithMany() + .WithMany("Secrets") .HasForeignKey("AppId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() @@ -3895,6 +3943,11 @@ namespace DysonNetwork.Sphere.Migrations b.Navigation("Articles"); }); + modelBuilder.Entity("DysonNetwork.Sphere.Developer.CustomApp", b => + { + b.Navigation("Secrets"); + }); + modelBuilder.Entity("DysonNetwork.Sphere.Permission.PermissionGroup", b => { b.Navigation("Members"); diff --git a/DysonNetwork.Sphere/Pages/Auth/Authorize.cshtml b/DysonNetwork.Sphere/Pages/Auth/Authorize.cshtml new file mode 100644 index 0000000..7ea1421 --- /dev/null +++ b/DysonNetwork.Sphere/Pages/Auth/Authorize.cshtml @@ -0,0 +1,127 @@ +@page "/auth/authorize" +@model DysonNetwork.Sphere.Pages.Auth.AuthorizeModel +@{ + ViewData["Title"] = "Authorize Application"; +} + +
+
+
+

+ Authorize Application +

+ @if (!string.IsNullOrEmpty(Model.AppName)) + { +
+
+ @if (!string.IsNullOrEmpty(Model.AppLogo)) + { +
+ @Model.AppName logo +
+ @Model.AppName?[0] +
+
+ } +
+

@Model.AppName

+ @if (!string.IsNullOrEmpty(Model.AppUri)) + { + + @Model.AppUri + + } +
+
+
+ } +

+ wants to access your account with the following permissions: +

+
+ +
+
    + @if (Model.Scope != null) + { + var scopeDescriptions = new Dictionary + { + ["openid"] = ("OpenID", "Read your basic profile information"), + ["profile"] = ("Profile", "View your basic profile information"), + ["email"] = ("Email", "View your email address"), + ["offline_access"] = ("Offline Access", "Access your data while you're not using the application") + }; + + foreach (var scope in Model.Scope.Split(' ').Where(s => !string.IsNullOrWhiteSpace(s))) + { + var scopeInfo = scopeDescriptions.GetValueOrDefault(scope, (scope, scope.Replace('_', ' '))); +
  • +
    +
    + +
    +
    +

    @scopeInfo.Item1

    +

    @scopeInfo.Item2

    +
    +
    +
  • + } + } +
+ +
+

By authorizing, you allow this application to access your information on your behalf.

+
+
+ +
+ + + + + + + + + + + +
+ + +
+ +
+

+ You can change these permissions later in your account settings. +

+
+
+
+
+ +@functions { + private string GetScopeDisplayName(string scope) + { + return scope switch + { + "openid" => "View your basic profile information", + "profile" => "View your profile information (name, picture, etc.)", + "email" => "View your email address", + "offline_access" => "Access your information while you're not using the app", + _ => scope + }; + } +} diff --git a/DysonNetwork.Sphere/Pages/Auth/Authorize.cshtml.cs b/DysonNetwork.Sphere/Pages/Auth/Authorize.cshtml.cs new file mode 100644 index 0000000..b626486 --- /dev/null +++ b/DysonNetwork.Sphere/Pages/Auth/Authorize.cshtml.cs @@ -0,0 +1,148 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using DysonNetwork.Sphere.Auth.OidcProvider.Services; +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; + +namespace DysonNetwork.Sphere.Pages.Auth; + +[Authorize] +public class AuthorizeModel(OidcProviderService oidcService) : PageModel +{ + [BindProperty(SupportsGet = true)] + public string? ReturnUrl { get; set; } + + [BindProperty(SupportsGet = true, Name = "client_id")] + [Required(ErrorMessage = "The client_id parameter is required")] + public string? ClientIdString { get; set; } + + public Guid ClientId { get; set; } + + [BindProperty(SupportsGet = true, Name = "response_type")] + public string ResponseType { get; set; } = "code"; + + [BindProperty(SupportsGet = true, Name = "redirect_uri")] + public string? RedirectUri { get; set; } + + [BindProperty(SupportsGet = true)] + public string? Scope { get; set; } + + [BindProperty(SupportsGet = true)] + public string? State { get; set; } + + [BindProperty(SupportsGet = true)] + public string? Nonce { get; set; } + + + [BindProperty(SupportsGet = true, Name = "code_challenge")] + public string? CodeChallenge { get; set; } + + [BindProperty(SupportsGet = true, Name = "code_challenge_method")] + public string? CodeChallengeMethod { get; set; } + + [BindProperty(SupportsGet = true, Name = "response_mode")] + public string? ResponseMode { get; set; } + + public string? AppName { get; set; } + public string? AppLogo { get; set; } + public string? AppUri { get; set; } + public string[]? RequestedScopes { get; set; } + + public async Task OnGetAsync() + { + if (string.IsNullOrEmpty(ClientIdString) || !Guid.TryParse(ClientIdString, out var clientId)) + { + ModelState.AddModelError("client_id", "Invalid client_id format"); + return BadRequest("Invalid client_id format"); + } + + ClientId = clientId; + + var client = await oidcService.FindClientByIdAsync(ClientId); + if (client == null) + { + ModelState.AddModelError("client_id", "Client not found"); + return NotFound("Client not found"); + } + + AppName = client.Name; + AppLogo = client.LogoUri; + AppUri = client.ClientUri; + RequestedScopes = (Scope ?? "openid profile").Split(' ').Distinct().ToArray(); + + return Page(); + } + + public async Task OnPostAsync(bool allow) + { + // First validate the client ID + if (string.IsNullOrEmpty(ClientIdString) || !Guid.TryParse(ClientIdString, out var clientId)) + { + ModelState.AddModelError("client_id", "Invalid client_id format"); + return BadRequest("Invalid client_id format"); + } + + ClientId = clientId; + + // Check if client exists + var client = await oidcService.FindClientByIdAsync(ClientId); + if (client == null) + { + ModelState.AddModelError("client_id", "Client not found"); + return NotFound("Client not found"); + } + + if (!allow) + { + // User denied the authorization request + if (string.IsNullOrEmpty(RedirectUri)) + return BadRequest("No redirect_uri provided"); + + var deniedUriBuilder = new UriBuilder(RedirectUri); + var deniedQuery = System.Web.HttpUtility.ParseQueryString(deniedUriBuilder.Query); + deniedQuery["error"] = "access_denied"; + deniedQuery["error_description"] = "The user denied the authorization request"; + if (!string.IsNullOrEmpty(State)) deniedQuery["state"] = State; + deniedUriBuilder.Query = deniedQuery.ToString(); + + return Redirect(deniedUriBuilder.ToString()); + } + + // User approved the request + if (string.IsNullOrEmpty(RedirectUri)) + { + ModelState.AddModelError("redirect_uri", "No redirect_uri provided"); + return BadRequest("No redirect_uri provided"); + } + + // Generate authorization code + var authCode = await oidcService.GenerateAuthorizationCodeAsync( + clientId: ClientId, + userId: User.Identity?.Name ?? string.Empty, + redirectUri: RedirectUri, + scopes: Scope?.Split(' ', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(), + codeChallenge: CodeChallenge, + codeChallengeMethod: CodeChallengeMethod, + nonce: Nonce); + + // Build the redirect URI with the authorization code + var redirectUri = new UriBuilder(RedirectUri); + var query = System.Web.HttpUtility.ParseQueryString(redirectUri.Query); + + // Add the authorization code + query["code"] = authCode; + + // Add state if provided (for CSRF protection) + if (!string.IsNullOrEmpty(State)) + { + query["state"] = State; + } + + // Set the query string + redirectUri.Query = query.ToString(); + + // Redirect back to the client with the authorization code + return Redirect(redirectUri.ToString()); + } +} diff --git a/DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml.cs b/DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml.cs index a7b0477..3c0dc79 100644 --- a/DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml.cs +++ b/DysonNetwork.Sphere/Pages/Auth/Challenge.cshtml.cs @@ -7,10 +7,13 @@ namespace DysonNetwork.Sphere.Pages.Auth { [BindProperty(SupportsGet = true)] public Guid Id { get; set; } + + [BindProperty(SupportsGet = true)] + public string? ReturnUrl { get; set; } public IActionResult OnGet() { - return RedirectToPage("SelectFactor", new { id = Id }); + return RedirectToPage("SelectFactor", new { id = Id, returnUrl = ReturnUrl }); } } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Pages/Auth/Login.cshtml b/DysonNetwork.Sphere/Pages/Auth/Login.cshtml index 3c1a418..5e07eb0 100644 --- a/DysonNetwork.Sphere/Pages/Auth/Login.cshtml +++ b/DysonNetwork.Sphere/Pages/Auth/Login.cshtml @@ -2,6 +2,7 @@ @model DysonNetwork.Sphere.Pages.Auth.LoginModel @{ ViewData["Title"] = "Login"; + var returnUrl = Model.ReturnUrl ?? ""; }
@@ -9,6 +10,7 @@

Login

+
diff --git a/DysonNetwork.Sphere/Pages/Auth/Login.cshtml.cs b/DysonNetwork.Sphere/Pages/Auth/Login.cshtml.cs index b49ad6f..6b12db9 100644 --- a/DysonNetwork.Sphere/Pages/Auth/Login.cshtml.cs +++ b/DysonNetwork.Sphere/Pages/Auth/Login.cshtml.cs @@ -18,6 +18,10 @@ namespace DysonNetwork.Sphere.Pages.Auth ) : PageModel { [BindProperty] [Required] public string Username { get; set; } = string.Empty; + + [BindProperty] + [FromQuery] + public string? ReturnUrl { get; set; } public void OnGet() { @@ -36,6 +40,12 @@ namespace DysonNetwork.Sphere.Pages.Auth ModelState.AddModelError(string.Empty, "Account was not found."); return Page(); } + + // Store the return URL in TempData to preserve it during the login flow + if (!string.IsNullOrEmpty(ReturnUrl) && Url.IsLocalUrl(ReturnUrl)) + { + TempData["ReturnUrl"] = ReturnUrl; + } var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString(); var userAgent = HttpContext.Request.Headers.UserAgent.ToString(); @@ -71,11 +81,13 @@ namespace DysonNetwork.Sphere.Pages.Auth await db.AuthChallenges.AddAsync(challenge); await db.SaveChangesAsync(); - als.CreateActionLogFromRequest(ActionLogType.ChallengeAttempt, - new Dictionary { { "challenge_id", challenge.Id } }, Request, account - ); - - return RedirectToPage("Challenge", new { id = challenge.Id }); + // If we have a return URL, pass it to the verify page + if (TempData.TryGetValue("ReturnUrl", out var returnUrl) && returnUrl is string url) + { + return RedirectToPage("SelectFactor", new { id = challenge.Id, returnUrl = url }); + } + + return RedirectToPage("SelectFactor", new { id = challenge.Id }); } } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Pages/Auth/SelectFactor.cshtml.cs b/DysonNetwork.Sphere/Pages/Auth/SelectFactor.cshtml.cs index 811fe83..e16a053 100644 --- a/DysonNetwork.Sphere/Pages/Auth/SelectFactor.cshtml.cs +++ b/DysonNetwork.Sphere/Pages/Auth/SelectFactor.cshtml.cs @@ -13,6 +13,9 @@ public class SelectFactorModel( : PageModel { [BindProperty(SupportsGet = true)] public Guid Id { get; set; } + [BindProperty(SupportsGet = true)] public string? ReturnUrl { get; set; } + [BindProperty] public Guid SelectedFactorId { get; set; } + [BindProperty] public string? Hint { get; set; } public Challenge? AuthChallenge { get; set; } public List AuthFactors { get; set; } = []; @@ -25,7 +28,7 @@ public class SelectFactorModel( return Page(); } - public async Task OnPostSelectFactorAsync(Guid factorId, string? hint = null) + public async Task OnPostSelectFactorAsync() { var challenge = await db.AuthChallenges .Include(e => e.Account) @@ -33,23 +36,29 @@ public class SelectFactorModel( if (challenge == null) return NotFound(); - var factor = await db.AccountAuthFactors.FindAsync(factorId); + var factor = await db.AccountAuthFactors.FindAsync(SelectedFactorId); if (factor?.EnabledAt == null || factor.Trustworthy <= 0) return BadRequest("Invalid authentication method."); + + // Store return URL in TempData to pass to the next step + if (!string.IsNullOrEmpty(ReturnUrl)) + { + TempData["ReturnUrl"] = ReturnUrl; + } // For OTP factors that require code delivery try { - // Validate hint for factors that require it + // For OTP factors that require code delivery if (factor.Type == AccountAuthFactorType.EmailCode - && string.IsNullOrWhiteSpace(hint)) + && string.IsNullOrWhiteSpace(Hint)) { ModelState.AddModelError(string.Empty, $"Please provide a {factor.Type.ToString().ToLower().Replace("code", "")} to send the code to."); await LoadChallengeAndFactors(); return Page(); } - await accounts.SendFactorCode(challenge.Account, factor, hint); + await accounts.SendFactorCode(challenge.Account, factor, Hint); } catch (Exception ex) { @@ -58,8 +67,12 @@ public class SelectFactorModel( return Page(); } - // Redirect to verify the page with the selected factor - return RedirectToPage("VerifyFactor", new { id = Id, factorId }); + // Redirect to verify page with return URL if available + if (!string.IsNullOrEmpty(ReturnUrl)) + { + return RedirectToPage("VerifyFactor", new { id = Id, factorId = factor.Id, returnUrl = ReturnUrl }); + } + return RedirectToPage("VerifyFactor", new { id = Id, factorId = factor.Id }); } private async Task LoadChallengeAndFactors() diff --git a/DysonNetwork.Sphere/Pages/Auth/VerifyFactor.cshtml.cs b/DysonNetwork.Sphere/Pages/Auth/VerifyFactor.cshtml.cs index 3d4fd88..82ab219 100644 --- a/DysonNetwork.Sphere/Pages/Auth/VerifyFactor.cshtml.cs +++ b/DysonNetwork.Sphere/Pages/Auth/VerifyFactor.cshtml.cs @@ -22,6 +22,9 @@ namespace DysonNetwork.Sphere.Pages.Auth [BindProperty(SupportsGet = true)] public Guid FactorId { get; set; } + + [BindProperty(SupportsGet = true)] + public string? ReturnUrl { get; set; } [BindProperty, Required] public string Code { get; set; } = string.Empty; @@ -159,21 +162,21 @@ namespace DysonNetwork.Sphere.Pages.Auth var session = await _db.AuthSessions .FirstOrDefaultAsync(e => e.ChallengeId == challenge.Id); - if (session != null) return BadRequest("Session already exists for this challenge."); - - session = new Session + if (session == null) { - LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow), - ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)), - Account = challenge.Account, - Challenge = challenge, - }; - - _db.AuthSessions.Add(session); - await _db.SaveChangesAsync(); + session = new Session + { + LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow), + ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)), + Account = challenge.Account, + Challenge = challenge, + }; + _db.AuthSessions.Add(session); + await _db.SaveChangesAsync(); + } var token = _auth.CreateToken(session); - Response.Cookies.Append(AuthConstants.CookieTokenName, token, new() + Response.Cookies.Append("access_token", token, new CookieOptions { HttpOnly = true, Secure = !_configuration.GetValue("Debug"), @@ -181,7 +184,19 @@ namespace DysonNetwork.Sphere.Pages.Auth Path = "/" }); - return RedirectToPage("/Account/Profile"); + // Redirect to the return URL if provided and valid, otherwise to the home page + if (!string.IsNullOrEmpty(ReturnUrl) && Url.IsLocalUrl(ReturnUrl)) + { + return Redirect(ReturnUrl); + } + + // Check TempData for return URL (in case it was passed through multiple steps) + if (TempData.TryGetValue("ReturnUrl", out var tempReturnUrl) && tempReturnUrl is string returnUrl && !string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl)) + { + return Redirect(returnUrl); + } + + return RedirectToPage("/Index"); } } } diff --git a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs index 7a560f6..b80becf 100644 --- a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs @@ -24,6 +24,7 @@ using NodaTime.Serialization.SystemTextJson; using StackExchange.Redis; using System.Text.Json; using System.Threading.RateLimiting; +using DysonNetwork.Sphere.Auth.OidcProvider.Services; using DysonNetwork.Sphere.Connection.WebReader; using DysonNetwork.Sphere.Developer; using DysonNetwork.Sphere.Discovery; @@ -234,6 +235,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/DysonNetwork.Sphere/wwwroot/css/styles.css b/DysonNetwork.Sphere/wwwroot/css/styles.css index 5837219..dca1395 100644 --- a/DysonNetwork.Sphere/wwwroot/css/styles.css +++ b/DysonNetwork.Sphere/wwwroot/css/styles.css @@ -17,6 +17,7 @@ --color-green-100: oklch(96.2% 0.044 156.743); --color-green-200: oklch(92.5% 0.084 155.995); --color-green-400: oklch(79.2% 0.209 151.711); + --color-green-500: oklch(72.3% 0.219 149.579); --color-green-600: oklch(62.7% 0.194 149.214); --color-green-800: oklch(44.8% 0.119 151.328); --color-green-900: oklch(39.3% 0.095 152.535); @@ -65,6 +66,7 @@ --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; + --font-weight-extrabold: 800; --tracking-tight: -0.025em; --radius-md: 0.375rem; --radius-lg: 0.5rem; @@ -229,12 +231,18 @@ .fixed { position: fixed; } + .relative { + position: relative; + } .static { position: static; } .sticky { position: sticky; } + .inset-0 { + inset: calc(var(--spacing) * 0); + } .top-0 { top: calc(var(--spacing) * 0); } @@ -322,6 +330,9 @@ .mb-8 { margin-bottom: calc(var(--spacing) * 8); } + .ml-3 { + margin-left: calc(var(--spacing) * 3); + } .ml-4 { margin-left: calc(var(--spacing) * 4); } @@ -352,9 +363,15 @@ .table { display: table; } + .h-5 { + height: calc(var(--spacing) * 5); + } .h-6 { height: calc(var(--spacing) * 6); } + .h-8 { + height: calc(var(--spacing) * 8); + } .h-10 { height: calc(var(--spacing) * 10); } @@ -373,12 +390,21 @@ .min-h-screen { min-height: 100vh; } + .w-5 { + width: calc(var(--spacing) * 5); + } .w-6 { width: calc(var(--spacing) * 6); } + .w-8 { + width: calc(var(--spacing) * 8); + } .w-10 { width: calc(var(--spacing) * 10); } + .w-16 { + width: calc(var(--spacing) * 16); + } .w-32 { width: calc(var(--spacing) * 32); } @@ -400,6 +426,9 @@ .flex-1 { flex: 1; } + .flex-shrink { + flex-shrink: 1; + } .flex-shrink-0 { flex-shrink: 0; } @@ -415,6 +444,12 @@ .resize { resize: both; } + .list-inside { + list-style-position: inside; + } + .list-disc { + list-style-type: disc; + } .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -424,6 +459,9 @@ .items-center { align-items: center; } + .items-start { + align-items: flex-start; + } .justify-around { justify-content: space-around; } @@ -442,6 +480,20 @@ .gap-8 { gap: calc(var(--spacing) * 8); } + .space-y-1 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-3 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse))); + } + } .space-y-4 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; @@ -456,6 +508,13 @@ margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); } } + .space-y-8 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse))); + } + } .gap-x-6 { column-gap: calc(var(--spacing) * 6); } @@ -552,6 +611,9 @@ .bg-yellow-100 { background-color: var(--color-yellow-100); } + .object-cover { + object-fit: cover; + } .p-4 { padding: calc(var(--spacing) * 4); } @@ -561,6 +623,9 @@ .p-8 { padding: calc(var(--spacing) * 8); } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } .px-2\.5 { padding-inline: calc(var(--spacing) * 2.5); } @@ -576,12 +641,18 @@ .px-8 { padding-inline: calc(var(--spacing) * 8); } + .py-0 { + padding-block: calc(var(--spacing) * 0); + } .py-0\.5 { padding-block: calc(var(--spacing) * 0.5); } .py-2 { padding-block: calc(var(--spacing) * 2); } + .py-2\.5 { + padding-block: calc(var(--spacing) * 2.5); + } .py-3 { padding-block: calc(var(--spacing) * 3); } @@ -597,6 +668,9 @@ .py-12 { padding-block: calc(var(--spacing) * 12); } + .pt-0\.5 { + padding-top: calc(var(--spacing) * 0.5); + } .pt-4 { padding-top: calc(var(--spacing) * 4); } @@ -662,6 +736,10 @@ --tw-font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold); } + .font-extrabold { + --tw-font-weight: var(--font-weight-extrabold); + font-weight: var(--font-weight-extrabold); + } .font-medium { --tw-font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium); @@ -692,6 +770,9 @@ .text-gray-900 { color: var(--color-gray-900); } + .text-green-500 { + color: var(--color-green-500); + } .text-green-600 { color: var(--color-green-600); } @@ -752,6 +833,14 @@ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } + .duration-150 { + --tw-duration: 150ms; + transition-duration: 150ms; + } + .duration-200 { + --tw-duration: 200ms; + transition-duration: 200ms; + } .hover\:scale-105 { &:hover { @media (hover: hover) { @@ -869,6 +958,21 @@ outline-style: none; } } + .sm\:mx-auto { + @media (width >= 40rem) { + margin-inline: auto; + } + } + .sm\:w-full { + @media (width >= 40rem) { + width: 100%; + } + } + .sm\:max-w-md { + @media (width >= 40rem) { + max-width: var(--container-md); + } + } .sm\:rounded-lg { @media (width >= 40rem) { border-radius: var(--radius-lg); @@ -879,6 +983,11 @@ padding-inline: calc(var(--spacing) * 6); } } + .sm\:px-10 { + @media (width >= 40rem) { + padding-inline: calc(var(--spacing) * 10); + } + } .sm\:text-6xl { @media (width >= 40rem) { font-size: var(--text-6xl); @@ -905,6 +1014,11 @@ width: calc(1/4 * 100%); } } + .lg\:px-8 { + @media (width >= 64rem) { + padding-inline: calc(var(--spacing) * 8); + } + } .dark\:divide-gray-700 { @media (prefers-color-scheme: dark) { :where(& > :not(:last-child)) { @@ -1001,6 +1115,15 @@ } } } + .dark\:hover\:bg-gray-700 { + @media (prefers-color-scheme: dark) { + &:hover { + @media (hover: hover) { + background-color: var(--color-gray-700); + } + } + } + } .dark\:hover\:text-blue-300 { @media (prefers-color-scheme: dark) { &:hover { @@ -1037,6 +1160,13 @@ } } } + .dark\:focus\:ring-offset-gray-800 { + @media (prefers-color-scheme: dark) { + &:focus { + --tw-ring-offset-color: var(--color-gray-800); + } + } + } } @layer theme, base, components, utilities; @layer theme; @@ -1429,6 +1559,10 @@ syntax: "*"; inherits: false; } +@property --tw-duration { + syntax: "*"; + inherits: false; +} @property --tw-scale-x { syntax: "*"; inherits: false; @@ -1486,6 +1620,7 @@ --tw-drop-shadow-color: initial; --tw-drop-shadow-alpha: 100%; --tw-drop-shadow-size: initial; + --tw-duration: initial; --tw-scale-x: 1; --tw-scale-y: 1; --tw-scale-z: 1;