♻️ Refactored the authorized device (now client)
This commit is contained in:
		@@ -1,4 +1,5 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using DysonNetwork.Pass.Auth;
 | 
			
		||||
using DysonNetwork.Pass.Permission;
 | 
			
		||||
using DysonNetwork.Pass.Wallet;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
@@ -9,7 +10,6 @@ using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using AuthService = DysonNetwork.Pass.Auth.AuthService;
 | 
			
		||||
using AuthSession = DysonNetwork.Pass.Auth.AuthSession;
 | 
			
		||||
using ChallengePlatform = DysonNetwork.Pass.Auth.ChallengePlatform;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Pass.Account;
 | 
			
		||||
 | 
			
		||||
@@ -437,25 +437,16 @@ public class AccountCurrentController(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class AuthorizedDevice
 | 
			
		||||
    {
 | 
			
		||||
        public string? Label { get; set; }
 | 
			
		||||
        public string UserAgent { get; set; } = null!;
 | 
			
		||||
        public string DeviceId { get; set; } = null!;
 | 
			
		||||
        public ChallengePlatform Platform { get; set; }
 | 
			
		||||
        public List<AuthSession> Sessions { get; set; } = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet("devices")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<ActionResult<List<AuthorizedDevice>>> GetDevices()
 | 
			
		||||
    public async Task<ActionResult<List<AuthClient>>> GetDevices()
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
 | 
			
		||||
            HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        Response.Headers.Append("X-Auth-Session", currentSession.Id.ToString());
 | 
			
		||||
 | 
			
		||||
        var devices = await db.AuthDevices
 | 
			
		||||
        var devices = await db.AuthClients
 | 
			
		||||
            .Where(device => device.AccountId == currentUser.Id)
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
 | 
			
		||||
@@ -525,14 +516,14 @@ public class AccountCurrentController(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPatch("sessions/{id:guid}/label")]
 | 
			
		||||
    public async Task<ActionResult<AuthSession>> UpdateSessionLabel(Guid id, [FromBody] string label)
 | 
			
		||||
    [HttpPatch("devices/{id}/label")]
 | 
			
		||||
    public async Task<ActionResult<AuthSession>> UpdateDeviceLabel(string id, [FromBody] string label)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await accounts.UpdateSessionLabel(currentUser, id, label);
 | 
			
		||||
            await accounts.UpdateDeviceName(currentUser, id, label);
 | 
			
		||||
            return NoContent();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
@@ -541,15 +532,18 @@ public class AccountCurrentController(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpPatch("sessions/current/label")]
 | 
			
		||||
    public async Task<ActionResult<AuthSession>> UpdateCurrentSessionLabel([FromBody] string label)
 | 
			
		||||
    [HttpPatch("devices/current/label")]
 | 
			
		||||
    public async Task<ActionResult<AuthSession>> UpdateCurrentDeviceLabel([FromBody] string label)
 | 
			
		||||
    {
 | 
			
		||||
        if (HttpContext.Items["CurrentUser"] is not Account currentUser ||
 | 
			
		||||
            HttpContext.Items["CurrentSession"] is not AuthSession currentSession) return Unauthorized();
 | 
			
		||||
 | 
			
		||||
        var device = await db.AuthClients.FirstOrDefaultAsync(d => d.Id == currentSession.Challenge.ClientId);
 | 
			
		||||
        if (device is null) return NotFound();
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await accounts.UpdateSessionLabel(currentUser, currentSession.Id, label);
 | 
			
		||||
            await accounts.UpdateDeviceName(currentUser, device.DeviceId, label);
 | 
			
		||||
            return NoContent();
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
@@ -637,7 +631,7 @@ public class AccountCurrentController(
 | 
			
		||||
            return BadRequest(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    [HttpPost("contacts/{id:guid}/public")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<ActionResult<AccountContact>> SetPublicContact(Guid id)
 | 
			
		||||
@@ -659,7 +653,7 @@ public class AccountCurrentController(
 | 
			
		||||
            return BadRequest(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    [HttpDelete("contacts/{id:guid}/public")]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public async Task<ActionResult<AccountContact>> UnsetPublicContact(Guid id)
 | 
			
		||||
@@ -733,4 +727,4 @@ public class AccountCurrentController(
 | 
			
		||||
            return BadRequest(ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,6 @@ using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.Extensions.Localization;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using OtpNet;
 | 
			
		||||
using AuthSession = DysonNetwork.Pass.Auth.AuthSession;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Pass.Account;
 | 
			
		||||
 | 
			
		||||
@@ -458,37 +457,28 @@ public class AccountService(
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> IsDeviceActive(Guid id)
 | 
			
		||||
    {
 | 
			
		||||
        return await db.AuthChallenges.AnyAsync(d => d.DeviceId == id);
 | 
			
		||||
        return await db.AuthSessions
 | 
			
		||||
            .Include(s => s.Challenge)
 | 
			
		||||
            .AnyAsync(s => s.Challenge.ClientId == id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<AuthSession> UpdateSessionLabel(Account account, Guid sessionId, string label)
 | 
			
		||||
    public async Task<AuthClient> UpdateDeviceName(Account account, string deviceId, string label)
 | 
			
		||||
    {
 | 
			
		||||
        var session = await db.AuthSessions
 | 
			
		||||
            .Include(s => s.Challenge)
 | 
			
		||||
            .Where(s => s.Id == sessionId && s.AccountId == account.Id)
 | 
			
		||||
            .FirstOrDefaultAsync();
 | 
			
		||||
        if (session is null) throw new InvalidOperationException("Session was not found.");
 | 
			
		||||
        var device = await db.AuthClients.FirstOrDefaultAsync(d => d.DeviceId == deviceId && d.AccountId == account.Id);
 | 
			
		||||
        if (device is null) throw new InvalidOperationException("Device was not found.");
 | 
			
		||||
 | 
			
		||||
        await db.AuthSessions
 | 
			
		||||
            .Include(s => s.Challenge)
 | 
			
		||||
            .Where(s => s.Challenge.DeviceId == session.Challenge.DeviceId)
 | 
			
		||||
            .ExecuteUpdateAsync(p => p.SetProperty(s => s.Label, label));
 | 
			
		||||
        device.DeviceLabel = label;
 | 
			
		||||
        db.Update(device);
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        var sessions = await db.AuthSessions
 | 
			
		||||
            .Include(s => s.Challenge)
 | 
			
		||||
            .Where(s => s.AccountId == session.Id && s.Challenge.DeviceId == session.Challenge.DeviceId)
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
        foreach (var item in sessions)
 | 
			
		||||
            await cache.RemoveAsync($"{DysonTokenAuthHandler.AuthCachePrefix}{item.Id}");
 | 
			
		||||
 | 
			
		||||
        return session;
 | 
			
		||||
        return device;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task DeleteSession(Account account, Guid sessionId)
 | 
			
		||||
    {
 | 
			
		||||
        var session = await db.AuthSessions
 | 
			
		||||
            .Include(s => s.Challenge)
 | 
			
		||||
            .ThenInclude(s => s.Device)
 | 
			
		||||
            .ThenInclude(s => s.Client)
 | 
			
		||||
            .Where(s => s.Id == sessionId && s.AccountId == account.Id)
 | 
			
		||||
            .FirstOrDefaultAsync();
 | 
			
		||||
        if (session is null) throw new InvalidOperationException("Session was not found.");
 | 
			
		||||
@@ -498,10 +488,13 @@ public class AccountService(
 | 
			
		||||
            .Where(s => s.AccountId == session.Id && s.Challenge.DeviceId == session.Challenge.DeviceId)
 | 
			
		||||
            .ToListAsync();
 | 
			
		||||
 | 
			
		||||
        if (!await IsDeviceActive(session.Challenge.DeviceId))
 | 
			
		||||
            await pusher.UnsubscribePushNotificationsAsync(new UnsubscribePushNotificationsRequest()
 | 
			
		||||
                { DeviceId = session.Challenge.Device.DeviceId }
 | 
			
		||||
            );
 | 
			
		||||
        if (session.Challenge.ClientId.HasValue)
 | 
			
		||||
        {
 | 
			
		||||
            if (!await IsDeviceActive(session.Challenge.ClientId.Value))
 | 
			
		||||
                await pusher.UnsubscribePushNotificationsAsync(new UnsubscribePushNotificationsRequest()
 | 
			
		||||
                    { DeviceId = session.Challenge.Client!.DeviceId }
 | 
			
		||||
                );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // The current session should be included in the sessions' list
 | 
			
		||||
        await db.AuthSessions
 | 
			
		||||
@@ -679,4 +672,4 @@ public class AccountService(
 | 
			
		||||
            await db.BulkInsertAsync(newProfiles);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -37,15 +37,15 @@ public class AppDatabase(
 | 
			
		||||
 | 
			
		||||
    public DbSet<AuthSession> AuthSessions { get; set; }
 | 
			
		||||
    public DbSet<AuthChallenge> AuthChallenges { get; set; }
 | 
			
		||||
    public DbSet<AuthDevice> AuthDevices { get; set; }
 | 
			
		||||
    
 | 
			
		||||
    public DbSet<AuthClient> AuthClients { get; set; }
 | 
			
		||||
 | 
			
		||||
    public DbSet<Wallet.Wallet> Wallets { get; set; }
 | 
			
		||||
    public DbSet<WalletPocket> WalletPockets { get; set; }
 | 
			
		||||
    public DbSet<Order> PaymentOrders { get; set; }
 | 
			
		||||
    public DbSet<Transaction> PaymentTransactions { get; set; }
 | 
			
		||||
    public DbSet<Subscription> WalletSubscriptions { get; set; }
 | 
			
		||||
    public DbSet<Coupon> WalletCoupons { get; set; }
 | 
			
		||||
 
 | 
			
		||||
 | 
			
		||||
    public DbSet<Punishment> Punishments { get; set; }
 | 
			
		||||
 | 
			
		||||
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
 | 
			
		||||
@@ -89,7 +89,7 @@ public class AppDatabase(
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        optionsBuilder.UseSeeding((context, _) => {});
 | 
			
		||||
        optionsBuilder.UseSeeding((context, _) => { });
 | 
			
		||||
 | 
			
		||||
        base.OnConfiguring(optionsBuilder);
 | 
			
		||||
    }
 | 
			
		||||
@@ -270,4 +270,4 @@ public static class OptionalQueryExtensions
 | 
			
		||||
    {
 | 
			
		||||
        return condition ? transform(source) : source;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@ public class AuthController(
 | 
			
		||||
            IpAddress = ipAddress,
 | 
			
		||||
            UserAgent = userAgent,
 | 
			
		||||
            Location = geo.GetPointFromIp(ipAddress),
 | 
			
		||||
            DeviceId = device.Id,
 | 
			
		||||
            ClientId = device.Id,
 | 
			
		||||
            AccountId = account.Id
 | 
			
		||||
        }.Normalize();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Pass.Auth;
 | 
			
		||||
 | 
			
		||||
[Index(nameof(DeviceId), IsUnique = true)]
 | 
			
		||||
public class AuthDevice : ModelBase
 | 
			
		||||
{
 | 
			
		||||
    public Guid Id { get; set; } = Guid.NewGuid();
 | 
			
		||||
    [MaxLength(1024)] public string DeviceName { get; set; } = string.Empty;
 | 
			
		||||
    [MaxLength(1024)] public string DeviceId { get; set; } = string.Empty;
 | 
			
		||||
    
 | 
			
		||||
    public Guid AccountId { get; set; }
 | 
			
		||||
    [JsonIgnore] public Account.Account Account { get; set; } = null!;
 | 
			
		||||
}
 | 
			
		||||
@@ -100,17 +100,17 @@ public class AuthService(
 | 
			
		||||
 | 
			
		||||
        return session;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task<AuthDevice> GetOrCreateDeviceAsync(Guid accountId, string deviceId)
 | 
			
		||||
 | 
			
		||||
    public async Task<AuthClient> GetOrCreateDeviceAsync(Guid accountId, string deviceId)
 | 
			
		||||
    {
 | 
			
		||||
        var device = await db.AuthDevices.FirstOrDefaultAsync(d => d.DeviceId == deviceId && d.AccountId == accountId);
 | 
			
		||||
        var device = await db.AuthClients.FirstOrDefaultAsync(d => d.DeviceId == deviceId && d.AccountId == accountId);
 | 
			
		||||
        if (device is not null) return device;
 | 
			
		||||
        device = new AuthDevice
 | 
			
		||||
        device = new AuthClient
 | 
			
		||||
        {
 | 
			
		||||
            DeviceId = deviceId,
 | 
			
		||||
            AccountId = accountId
 | 
			
		||||
        };
 | 
			
		||||
        db.AuthDevices.Add(device);
 | 
			
		||||
        db.AuthClients.Add(device);
 | 
			
		||||
        await db.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
        return device;
 | 
			
		||||
@@ -203,43 +203,43 @@ public class AuthService(
 | 
			
		||||
        // Check if the session is already in sudo mode (cached)
 | 
			
		||||
        var sudoModeKey = $"accounts:{session.Id}:sudo";
 | 
			
		||||
        var (found, _) = await cache.GetAsyncWithStatus<bool>(sudoModeKey);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        if (found)
 | 
			
		||||
        {
 | 
			
		||||
            // Session is already in sudo mode
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // Check if the user has a pin code
 | 
			
		||||
        var hasPinCode = await db.AccountAuthFactors
 | 
			
		||||
            .Where(f => f.AccountId == session.AccountId)
 | 
			
		||||
            .Where(f => f.EnabledAt != null)
 | 
			
		||||
            .Where(f => f.Type == AccountAuthFactorType.PinCode)
 | 
			
		||||
            .AnyAsync();
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
        if (!hasPinCode)
 | 
			
		||||
        {
 | 
			
		||||
            // User doesn't have a pin code, no validation needed
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // If pin code is not provided, we can't validate
 | 
			
		||||
        if (string.IsNullOrEmpty(pinCode))
 | 
			
		||||
        {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            // Validate the pin code
 | 
			
		||||
            var isValid = await ValidatePinCode(session.AccountId, pinCode);
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            if (isValid)
 | 
			
		||||
            {
 | 
			
		||||
                // Set session in sudo mode for 5 minutes
 | 
			
		||||
                await cache.SetAsync(sudoModeKey, true, TimeSpan.FromMinutes(5));
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            return isValid;
 | 
			
		||||
        }
 | 
			
		||||
        catch (InvalidOperationException)
 | 
			
		||||
@@ -316,4 +316,4 @@ public class AuthService(
 | 
			
		||||
 | 
			
		||||
        return Convert.FromBase64String(padded);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
using DysonNetwork.Shared.Data;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
using NodaTime.Serialization.Protobuf;
 | 
			
		||||
using Point = NetTopologySuite.Geometries.Point;
 | 
			
		||||
@@ -68,12 +69,13 @@ public class AuthChallenge : ModelBase
 | 
			
		||||
    [MaxLength(128)] public string? IpAddress { get; set; }
 | 
			
		||||
    [MaxLength(512)] public string? UserAgent { get; set; }
 | 
			
		||||
    [MaxLength(1024)] public string? Nonce { get; set; }
 | 
			
		||||
    [MaxLength(1024)] public string? DeviceId { get; set; } = string.Empty;
 | 
			
		||||
    public Point? Location { get; set; }
 | 
			
		||||
 | 
			
		||||
    public Guid AccountId { get; set; }
 | 
			
		||||
    [JsonIgnore] public Account.Account Account { get; set; } = null!;
 | 
			
		||||
    public Guid DeviceId { get; set; }
 | 
			
		||||
    public AuthDevice Device { get; set; } = null!;
 | 
			
		||||
    public Guid? ClientId { get; set; }
 | 
			
		||||
    public AuthClient? Client { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
    public AuthChallenge Normalize()
 | 
			
		||||
    {
 | 
			
		||||
@@ -95,8 +97,20 @@ public class AuthChallenge : ModelBase
 | 
			
		||||
        Scopes = { Scopes },
 | 
			
		||||
        IpAddress = IpAddress,
 | 
			
		||||
        UserAgent = UserAgent,
 | 
			
		||||
        DeviceId = DeviceId.ToString(),
 | 
			
		||||
        DeviceId = Client.DeviceId.ToString(),
 | 
			
		||||
        Nonce = Nonce,
 | 
			
		||||
        AccountId = AccountId.ToString()
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[Index(nameof(DeviceId), IsUnique = true)]
 | 
			
		||||
public class AuthClient : ModelBase
 | 
			
		||||
{
 | 
			
		||||
    public Guid Id { get; set; } = Guid.NewGuid();
 | 
			
		||||
    [MaxLength(1024)] public string DeviceName { get; set; } = string.Empty;
 | 
			
		||||
    [MaxLength(1024)] public string? DeviceLabel { get; set; }
 | 
			
		||||
    [MaxLength(1024)] public string DeviceId { get; set; } = string.Empty;
 | 
			
		||||
 | 
			
		||||
    public Guid AccountId { get; set; }
 | 
			
		||||
    [JsonIgnore] public Account.Account Account { get; set; } = null!;
 | 
			
		||||
}
 | 
			
		||||
@@ -227,7 +227,7 @@ public abstract class OidcService(
 | 
			
		||||
            Audiences = [ProviderName],
 | 
			
		||||
            Scopes = ["*"],
 | 
			
		||||
            AccountId = account.Id,
 | 
			
		||||
            DeviceId = device.Id,
 | 
			
		||||
            ClientId = device.Id,
 | 
			
		||||
            IpAddress = request.Connection.RemoteIpAddress?.ToString() ?? null,
 | 
			
		||||
            UserAgent = request.Request.Headers.UserAgent,
 | 
			
		||||
        };
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1830
									
								
								DysonNetwork.Pass/Migrations/20250813072436_AddAuthorizeDevice.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1830
									
								
								DysonNetwork.Pass/Migrations/20250813072436_AddAuthorizeDevice.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -0,0 +1,110 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using NodaTime;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace DysonNetwork.Pass.Migrations
 | 
			
		||||
{
 | 
			
		||||
    /// <inheritdoc />
 | 
			
		||||
    public partial class AddAuthorizeDevice : Migration
 | 
			
		||||
    {
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "device_id",
 | 
			
		||||
                table: "auth_challenges",
 | 
			
		||||
                type: "character varying(1024)",
 | 
			
		||||
                maxLength: 1024,
 | 
			
		||||
                nullable: true,
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "character varying(256)",
 | 
			
		||||
                oldMaxLength: 256,
 | 
			
		||||
                oldNullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddColumn<Guid>(
 | 
			
		||||
                name: "client_id",
 | 
			
		||||
                table: "auth_challenges",
 | 
			
		||||
                type: "uuid",
 | 
			
		||||
                nullable: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "auth_clients",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    device_name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
			
		||||
                    device_label = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
 | 
			
		||||
                    device_id = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
 | 
			
		||||
                    account_id = table.Column<Guid>(type: "uuid", nullable: false),
 | 
			
		||||
                    created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_auth_clients", x => x.id);
 | 
			
		||||
                    table.ForeignKey(
 | 
			
		||||
                        name: "fk_auth_clients_accounts_account_id",
 | 
			
		||||
                        column: x => x.account_id,
 | 
			
		||||
                        principalTable: "accounts",
 | 
			
		||||
                        principalColumn: "id",
 | 
			
		||||
                        onDelete: ReferentialAction.Cascade);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_auth_challenges_client_id",
 | 
			
		||||
                table: "auth_challenges",
 | 
			
		||||
                column: "client_id");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_auth_clients_account_id",
 | 
			
		||||
                table: "auth_clients",
 | 
			
		||||
                column: "account_id");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_auth_clients_device_id",
 | 
			
		||||
                table: "auth_clients",
 | 
			
		||||
                column: "device_id",
 | 
			
		||||
                unique: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AddForeignKey(
 | 
			
		||||
                name: "fk_auth_challenges_auth_clients_client_id",
 | 
			
		||||
                table: "auth_challenges",
 | 
			
		||||
                column: "client_id",
 | 
			
		||||
                principalTable: "auth_clients",
 | 
			
		||||
                principalColumn: "id");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropForeignKey(
 | 
			
		||||
                name: "fk_auth_challenges_auth_clients_client_id",
 | 
			
		||||
                table: "auth_challenges");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "auth_clients");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropIndex(
 | 
			
		||||
                name: "ix_auth_challenges_client_id",
 | 
			
		||||
                table: "auth_challenges");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropColumn(
 | 
			
		||||
                name: "client_id",
 | 
			
		||||
                table: "auth_challenges");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<string>(
 | 
			
		||||
                name: "device_id",
 | 
			
		||||
                table: "auth_challenges",
 | 
			
		||||
                type: "character varying(256)",
 | 
			
		||||
                maxLength: 256,
 | 
			
		||||
                nullable: true,
 | 
			
		||||
                oldClrType: typeof(string),
 | 
			
		||||
                oldType: "character varying(1024)",
 | 
			
		||||
                oldMaxLength: 1024,
 | 
			
		||||
                oldNullable: true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -435,7 +435,7 @@ namespace DysonNetwork.Pass.Migrations
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("last_seen_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Dictionary<string, string>>("Links")
 | 
			
		||||
                    b.Property<List<ProfileLink>>("Links")
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("links");
 | 
			
		||||
 | 
			
		||||
@@ -817,6 +817,10 @@ namespace DysonNetwork.Pass.Migrations
 | 
			
		||||
                        .HasColumnType("jsonb")
 | 
			
		||||
                        .HasColumnName("blacklist_factors");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid?>("ClientId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("client_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
@@ -826,8 +830,8 @@ namespace DysonNetwork.Pass.Migrations
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("DeviceId")
 | 
			
		||||
                        .HasMaxLength(256)
 | 
			
		||||
                        .HasColumnType("character varying(256)")
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("device_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("ExpiredAt")
 | 
			
		||||
@@ -888,9 +892,65 @@ namespace DysonNetwork.Pass.Migrations
 | 
			
		||||
                    b.HasIndex("AccountId")
 | 
			
		||||
                        .HasDatabaseName("ix_auth_challenges_account_id");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("ClientId")
 | 
			
		||||
                        .HasDatabaseName("ix_auth_challenges_client_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("auth_challenges", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthClient", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Guid>("AccountId")
 | 
			
		||||
                        .HasColumnType("uuid")
 | 
			
		||||
                        .HasColumnName("account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("CreatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("created_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant?>("DeletedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("deleted_at");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("DeviceId")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("device_id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("DeviceLabel")
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("device_label");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("DeviceName")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasMaxLength(1024)
 | 
			
		||||
                        .HasColumnType("character varying(1024)")
 | 
			
		||||
                        .HasColumnName("device_name");
 | 
			
		||||
 | 
			
		||||
                    b.Property<Instant>("UpdatedAt")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("updated_at");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_auth_clients");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("AccountId")
 | 
			
		||||
                        .HasDatabaseName("ix_auth_clients_account_id");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("DeviceId")
 | 
			
		||||
                        .IsUnique()
 | 
			
		||||
                        .HasDatabaseName("ix_auth_clients_device_id");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("auth_clients", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthSession", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<Guid>("Id")
 | 
			
		||||
@@ -1586,6 +1646,25 @@ namespace DysonNetwork.Pass.Migrations
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_auth_challenges_accounts_account_id");
 | 
			
		||||
 | 
			
		||||
                    b.HasOne("DysonNetwork.Pass.Auth.AuthClient", "Client")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("ClientId")
 | 
			
		||||
                        .HasConstraintName("fk_auth_challenges_auth_clients_client_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Account");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Client");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("DysonNetwork.Pass.Auth.AuthClient", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("DysonNetwork.Pass.Account.Account", "Account")
 | 
			
		||||
                        .WithMany()
 | 
			
		||||
                        .HasForeignKey("AccountId")
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade)
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasConstraintName("fk_auth_clients_accounts_account_id");
 | 
			
		||||
 | 
			
		||||
                    b.Navigation("Account");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,7 @@
 | 
			
		||||
	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcerExtension_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003Fb5_003F180850e0_003FEnforcerExtension_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 | 
			
		||||
	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnforcer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbb4a120e56464fc6abd8c30969ef70864ba00_003F47_003F3a6b6c4b_003FEnforcer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 | 
			
		||||
	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe096e6f12c5d6b49356bc34ff1ea08738f910c0929c9d717c9cba7f44288_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 | 
			
		||||
	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F363c1765261146f1a68840a2d3ce7e39291438_003F2a_003F960244de_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 | 
			
		||||
	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkQueryableExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcb0587797ea44bd6915ede69888c6766291038_003F55_003F277f2d4c_003FEntityFrameworkQueryableExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 | 
			
		||||
	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEntityFrameworkServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4a28847852ee9ba45fd3107526c0a749a733bd4f4ebf33aa3c9a59737a3f758_003FEntityFrameworkServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 | 
			
		||||
	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEnumerable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F832399abc13b45b6bdbabfa022e4a28487e00_003F7f_003F7aece4dd_003FEnumerable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user