diff --git a/PlaylistShared.Api/Controllers/YandexAccountController.cs b/PlaylistShared.Api/Controllers/YandexAccountController.cs index 7ae730e..c7af3bc 100644 --- a/PlaylistShared.Api/Controllers/YandexAccountController.cs +++ b/PlaylistShared.Api/Controllers/YandexAccountController.cs @@ -64,7 +64,7 @@ public class YandexAccountController : ControllerBase var user = await _userManager.FindByIdAsync(userId.ToString()); if (user == null) return Unauthorized(); - var qr = await _yandexService.GenerateQrAsync(user); + var qr = await _yandexService.GetQrOrGenerate(user); return Ok(ApiResponse.Ok(qr)); } diff --git a/PlaylistShared.Api/Data/ApplicationDbContext.cs b/PlaylistShared.Api/Data/ApplicationDbContext.cs index cf57941..14c33bd 100644 --- a/PlaylistShared.Api/Data/ApplicationDbContext.cs +++ b/PlaylistShared.Api/Data/ApplicationDbContext.cs @@ -131,6 +131,12 @@ public class ApplicationDbContext : IdentityDbContext e.CsfrToken) .HasMaxLength(200) .IsRequired(false); + entity.Property(e => e.HeaderCsfrToken) + .HasMaxLength(200) + .IsRequired(false); + entity.Property(e => e.HeaderProcessId) + .HasMaxLength(200) + .IsRequired(false); entity.HasIndex(e => e.UserId) .HasDatabaseName("IX_YandexAuthSessions_UserId"); }); diff --git a/PlaylistShared.Api/Data/Migrations/20260420123450_AddYandexAuthSessions_Header.Designer.cs b/PlaylistShared.Api/Data/Migrations/20260420123450_AddYandexAuthSessions_Header.Designer.cs new file mode 100644 index 0000000..ecc87cc --- /dev/null +++ b/PlaylistShared.Api/Data/Migrations/20260420123450_AddYandexAuthSessions_Header.Designer.cs @@ -0,0 +1,676 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PlaylistShared.Api.Data; + +#nullable disable + +namespace PlaylistShared.Api.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260420123450_AddYandexAuthSessions_Header")] + partial class AddYandexAuthSessions_Header + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FriendlyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Xml") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("RefreshToken") + .HasColumnType("nvarchar(max)"); + + b.Property("RefreshTokenExpiryUtc") + .HasColumnType("datetime2"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("YandexAccessToken") + .HasColumnType("nvarchar(max)"); + + b.Property("YandexId") + .HasColumnType("nvarchar(max)"); + + b.Property("YandexRefreshToken") + .HasColumnType("nvarchar(max)"); + + b.Property("YandexTokenExpiryUtc") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("SharedPlaylistId") + .HasColumnType("uniqueidentifier"); + + b.Property("AddedAtUtc") + .HasColumnType("datetime2"); + + b.HasKey("UserId", "SharedPlaylistId"); + + b.HasIndex("SharedPlaylistId"); + + b.ToTable("FavoritePlaylists"); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddPermission") + .HasColumnType("int"); + + b.Property("CoverUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PlayPermission") + .HasColumnType("int"); + + b.Property("RemovePermission") + .HasColumnType("int"); + + b.Property("ShareToken") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("ViewPermission") + .HasColumnType("int"); + + b.Property("YandexPlaylistKind") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("YandexPlaylistOwnerUid") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("YandexPlaylistUuid") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatorUserId"); + + b.HasIndex("ShareToken") + .IsUnique(); + + b.ToTable("SharedPlaylists"); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddedAtUtc") + .HasColumnType("datetime2"); + + b.Property("AddedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(449)"); + + b.Property("SharedPlaylistId") + .HasColumnType("uniqueidentifier"); + + b.Property("TrackId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("AddedByUserId"); + + b.HasIndex("SessionId"); + + b.HasIndex("SharedPlaylistId", "TrackId"); + + b.ToTable("TrackAdditionLogs"); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b => + { + b.Property("SessionId") + .HasMaxLength(449) + .HasColumnType("nvarchar(449)"); + + b.Property("AssociatedUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("ClientIpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("FirstSeenUtc") + .HasColumnType("datetime2"); + + b.Property("LastSeenUtc") + .HasColumnType("datetime2"); + + b.Property("UserAgent") + .HasColumnType("nvarchar(max)"); + + b.HasKey("SessionId"); + + b.HasIndex("AssociatedUserId"); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.YandexAuthSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConfirmedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CsfrToken") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HeaderCsfrToken") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HeaderProcessId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsConfirmed") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("QrCodeUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SerializedCookies") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TrackId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_YandexAuthSessions_UserId"); + + b.ToTable("YandexAuthSessions"); + }); + + modelBuilder.Entity("TrackRemovalLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("RemovedAtUtc") + .HasColumnType("datetime2"); + + b.Property("RemovedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(449)"); + + b.Property("SharedPlaylistId") + .HasColumnType("uniqueidentifier"); + + b.Property("TrackId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RemovedByUserId"); + + b.HasIndex("SessionId"); + + b.HasIndex("SharedPlaylistId", "TrackId"); + + b.ToTable("TrackRemovalLogs"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.FavoritePlaylist", b => + { + b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist") + .WithMany() + .HasForeignKey("SharedPlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User") + .WithMany("FavoritePlaylists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SharedPlaylist"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b => + { + b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator") + .WithMany("OwnedPlaylists") + .HasForeignKey("CreatorUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b => + { + b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser") + .WithMany() + .HasForeignKey("AddedByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session") + .WithMany("TrackAdditionLogs") + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist") + .WithMany("TrackAdditionLogs") + .HasForeignKey("SharedPlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AddedByUser"); + + b.Navigation("Session"); + + b.Navigation("SharedPlaylist"); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b => + { + b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User") + .WithMany() + .HasForeignKey("AssociatedUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.YandexAuthSession", b => + { + b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TrackRemovalLog", b => + { + b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "RemovedByUser") + .WithMany() + .HasForeignKey("RemovedByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session") + .WithMany("TrackRemovalLogs") + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist") + .WithMany() + .HasForeignKey("SharedPlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RemovedByUser"); + + b.Navigation("Session"); + + b.Navigation("SharedPlaylist"); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b => + { + b.Navigation("FavoritePlaylists"); + + b.Navigation("OwnedPlaylists"); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b => + { + b.Navigation("TrackAdditionLogs"); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b => + { + b.Navigation("TrackAdditionLogs"); + + b.Navigation("TrackRemovalLogs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PlaylistShared.Api/Data/Migrations/20260420123450_AddYandexAuthSessions_Header.cs b/PlaylistShared.Api/Data/Migrations/20260420123450_AddYandexAuthSessions_Header.cs new file mode 100644 index 0000000..cc865dc --- /dev/null +++ b/PlaylistShared.Api/Data/Migrations/20260420123450_AddYandexAuthSessions_Header.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PlaylistShared.Api.Data.Migrations +{ + /// + public partial class AddYandexAuthSessions_Header : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "HeaderCsfrToken", + table: "YandexAuthSessions", + type: "nvarchar(200)", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "HeaderProcessId", + table: "YandexAuthSessions", + type: "nvarchar(200)", + maxLength: 200, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "HeaderCsfrToken", + table: "YandexAuthSessions"); + + migrationBuilder.DropColumn( + name: "HeaderProcessId", + table: "YandexAuthSessions"); + } + } +} diff --git a/PlaylistShared.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/PlaylistShared.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs index dba31a1..1864638 100644 --- a/PlaylistShared.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/PlaylistShared.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -425,6 +425,14 @@ namespace PlaylistShared.Api.Data.Migrations .HasMaxLength(200) .HasColumnType("nvarchar(200)"); + b.Property("HeaderCsfrToken") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("HeaderProcessId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + b.Property("IsConfirmed") .ValueGeneratedOnAdd() .HasColumnType("bit") diff --git a/PlaylistShared.Api/Entities/YandexAuthSession.cs b/PlaylistShared.Api/Entities/YandexAuthSession.cs index ccf5345..2becf4e 100644 --- a/PlaylistShared.Api/Entities/YandexAuthSession.cs +++ b/PlaylistShared.Api/Entities/YandexAuthSession.cs @@ -11,6 +11,8 @@ public class YandexAuthSession public bool IsConfirmed { get; set; } public string? TrackId { get; set; } public string? CsfrToken { get; set; } + public string? HeaderProcessId { get; set; } + public string? HeaderCsfrToken { get; set; } public ApplicationUser? User { get; set; } } \ No newline at end of file diff --git a/PlaylistShared.Api/PlaylistShared.Api.csproj b/PlaylistShared.Api/PlaylistShared.Api.csproj index cc00d48..f7d4a24 100644 --- a/PlaylistShared.Api/PlaylistShared.Api.csproj +++ b/PlaylistShared.Api/PlaylistShared.Api.csproj @@ -27,7 +27,7 @@ - + diff --git a/PlaylistShared.Api/Services/Yandex/YandexApiService.cs b/PlaylistShared.Api/Services/Yandex/YandexApiService.cs index 4c0c971..7399135 100644 --- a/PlaylistShared.Api/Services/Yandex/YandexApiService.cs +++ b/PlaylistShared.Api/Services/Yandex/YandexApiService.cs @@ -2,7 +2,6 @@ using PlaylistShared.Api.Entities; using System.Net; using YandexMusic; -using YandexMusic.API.Common; namespace PlaylistShared.Api.Services; @@ -12,7 +11,6 @@ namespace PlaylistShared.Api.Services; public class YandexApiService : IDisposable { private readonly IDataProtector _dataProtector; - private readonly HttpClient _httpClient; private readonly YandexMusicClient _client; private readonly CookieContainer _cookieContainer; @@ -33,12 +31,7 @@ public class YandexApiService : IDisposable { _dataProtector = provider.CreateProtector("YandexTokens"); _cookieContainer = new(); - _httpClient = YandexMusicHttpClientFactory.CreateDefault( - cookieContainer: _cookieContainer, - proxy: proxy, - timeout: timeout - ); - _client = new YandexMusicClient(_httpClient); + _client = new YandexMusicClient(_cookieContainer, proxy, timeout); } @@ -107,21 +100,6 @@ public class YandexApiService : IDisposable return cookie?.Value; } - private void UpdateHttpClientCookieContainer(CookieContainer container) - { - var handler = GetInnerHandler(_httpClient); - if (handler is HttpClientHandler httpHandler) - httpHandler.CookieContainer = container; - } - - private static HttpMessageHandler GetInnerHandler(HttpClient client) - { - var field = client.GetType().GetField("_handler", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - if (field?.GetValue(client) is HttpMessageHandler handler) - return handler; - return new HttpClientHandler(); - } - /// /// Авторизуется с помощью OAuth-токена. /// @@ -133,6 +111,5 @@ public class YandexApiService : IDisposable public void Dispose() { _client.Dispose(); - _httpClient.Dispose(); } } \ No newline at end of file diff --git a/PlaylistShared.Api/Services/Yandex/YandexAuthService.cs b/PlaylistShared.Api/Services/Yandex/YandexAuthService.cs index 7a91d06..8e85c99 100644 --- a/PlaylistShared.Api/Services/Yandex/YandexAuthService.cs +++ b/PlaylistShared.Api/Services/Yandex/YandexAuthService.cs @@ -1,9 +1,9 @@ -using PlaylistShared.Api.Data; +using Microsoft.EntityFrameworkCore; +using PlaylistShared.Api.Data; using PlaylistShared.Api.Entities; using PlaylistShared.Shared.Yandex; using System.Net; using System.Text.Json; -using YandexMusic.API.Models.Account; namespace PlaylistShared.Api.Services; @@ -20,12 +20,33 @@ public class YandexAuthService _dbContext = dbContext; } + internal async Task GetQrOrGenerate(ApplicationUser user) + { + var existingSession = _dbContext.YandexAuthSessions + .Where(s => s.UserId == user.Id && !s.IsConfirmed && s.CreatedAt > DateTime.UtcNow.AddMinutes(-5)) + .OrderByDescending(s => s.CreatedAt) + .FirstOrDefault(); + + if (existingSession != null) + { + return new YandexAuthQr + { + QrLink = existingSession.QrCodeUrl, + SessionId = existingSession.Id.ToString() + }; + } + + return await GenerateQrAsync(user); + } + internal async Task GenerateQrAsync(ApplicationUser user) { var client = _apiService.Client; var qr = await client.GetAuthQRLink(); var trackId = client.AuthStorage.AuthToken.TrackId; var csfrToken = client.AuthStorage.AuthToken.CsfrToken; + var headerProcessUuid = client.AuthStorage.HeaderToken.ProcessUuid; + var headerCsfrToken = client.AuthStorage.HeaderToken.CsfrToken; if (string.IsNullOrEmpty(qr)) throw new Exception("Не удалось получить QR-ссылку"); @@ -41,6 +62,8 @@ public class YandexAuthService IsConfirmed = false, TrackId = trackId, CsfrToken = csfrToken, + HeaderCsfrToken = headerCsfrToken, + HeaderProcessId = headerProcessUuid, }; @@ -67,19 +90,26 @@ public class YandexAuthService _apiService.Client.AuthStorage.AuthToken.CsfrToken = session?.CsfrToken ?? ""; _apiService.Client.AuthStorage.AuthToken.TrackId = session?.TrackId ?? ""; + _apiService.Client.AuthStorage.HeaderToken.CsfrToken = session?.HeaderCsfrToken ?? ""; + _apiService.Client.AuthStorage.HeaderToken.ProcessUuid = session?.HeaderProcessId ?? ""; - var status = await _apiService.Client.AuthorizeByQR(); + var status = await _apiService.Client.CheckQRStatusAsync(); - if (status?.Status == YAuthStatus.Ok) + if (status?.State == "otp_auth_finished") { - session.ConfirmedAt = DateTime.UtcNow; - session.IsConfirmed = true; - await _dbContext.SaveChangesAsync(); - - return new() + try { - Status = Shared.Enums.YandexAuthQrStatus.Authorized, - }; + var auth = await _apiService.Client.AuthorizeByQR(); + } + catch (Exception ex) + { + return new() { Status = Shared.Enums.YandexAuthQrStatus.Error, }; + } + + _dbContext.YandexAuthSessions.Where(t => t.UserId == session.UserId).ExecuteDelete(); + _dbContext.SaveChanges(); + + return new() { Status = Shared.Enums.YandexAuthQrStatus.Authorized, }; } return new() @@ -91,7 +121,6 @@ public class YandexAuthService private string SerializeCookies(CookieContainer container) { - var domains = new[] { "yandex.ru", "passport.yandex.ru", ".yandex.ru" }; var allCookies = new List(); var cookies = container.GetAllCookies(); @@ -108,8 +137,7 @@ public class YandexAuthService var cookies = JsonSerializer.Deserialize>(serializedCookies); foreach (var c in cookies) { - var uri = new Uri($"{c.Domain}"); - container.Add(uri, new Cookie(c.Name, c.Value, c.Path, c.Domain)); + container.Add(new Cookie(c.Name, c.Value, c.Path, c.Domain)); } } diff --git a/PlaylistShared.Pwa/Components/Profile/YandexAccount/YandexQrDialog.razor b/PlaylistShared.Pwa/Components/Profile/YandexAccount/YandexQrServerDialog.razor similarity index 97% rename from PlaylistShared.Pwa/Components/Profile/YandexAccount/YandexQrDialog.razor rename to PlaylistShared.Pwa/Components/Profile/YandexAccount/YandexQrServerDialog.razor index 34bac82..b746b08 100644 --- a/PlaylistShared.Pwa/Components/Profile/YandexAccount/YandexQrDialog.razor +++ b/PlaylistShared.Pwa/Components/Profile/YandexAccount/YandexQrServerDialog.razor @@ -88,7 +88,7 @@ { try { - await Task.Delay(2000, token); + await Task.Delay(500, token); var statusResponse = await Http.GetFromJsonAsync>($"/api/yandexaccount/qr/{_sessionId}", token); if (statusResponse?.Data != null) { @@ -101,13 +101,15 @@ _statusText = "Авторизация успешна!"; _isWaiting = false; Snackbar.Add("Авторизация выполнена", Severity.Success); - MudDialog.Close(DialogResult.Ok(true)); _cts?.Cancel(); + MudDialog.Close(DialogResult.Ok(true)); return; case Shared.Enums.YandexAuthQrStatus.Expired: + _cts?.Cancel(); ShowError("Срок действия QR-кода истёк"); return; case Shared.Enums.YandexAuthQrStatus.Error: + _cts?.Cancel(); ShowError("Ошибка авторизации"); return; } diff --git a/PlaylistShared.Pwa/Pages/Profile.razor b/PlaylistShared.Pwa/Pages/Profile.razor index 6b23252..d7e5da3 100644 --- a/PlaylistShared.Pwa/Pages/Profile.razor +++ b/PlaylistShared.Pwa/Pages/Profile.razor @@ -39,7 +39,7 @@ Dense Color="Color.Primary"> Token - Qr + QR @@ -77,10 +77,10 @@ if (!result.Canceled) await LoadStatus(); } - private async Task OpenQrDialog() + private async Task OpenQrServerDialog() { var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }; - var dialog = await DialogService.ShowAsync("", options); + var dialog = await DialogService.ShowAsync("", options); var result = await dialog.Result; if (!result.Canceled) await LoadStatus();