Доработана QR авторизация

This commit is contained in:
FrigaT
2026-04-20 16:06:47 +03:00
parent 12241639dc
commit 9c95e6b189
11 changed files with 784 additions and 45 deletions

View File

@@ -64,7 +64,7 @@ public class YandexAccountController : ControllerBase
var user = await _userManager.FindByIdAsync(userId.ToString()); var user = await _userManager.FindByIdAsync(userId.ToString());
if (user == null) return Unauthorized(); if (user == null) return Unauthorized();
var qr = await _yandexService.GenerateQrAsync(user); var qr = await _yandexService.GetQrOrGenerate(user);
return Ok(ApiResponse<YandexAuthQr>.Ok(qr)); return Ok(ApiResponse<YandexAuthQr>.Ok(qr));
} }

View File

@@ -131,6 +131,12 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
entity.Property(e => e.CsfrToken) entity.Property(e => e.CsfrToken)
.HasMaxLength(200) .HasMaxLength(200)
.IsRequired(false); .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) entity.HasIndex(e => e.UserId)
.HasDatabaseName("IX_YandexAuthSessions_UserId"); .HasDatabaseName("IX_YandexAuthSessions_UserId");
}); });

View File

@@ -0,0 +1,676 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("FriendlyName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Xml")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("DataProtectionKeys");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("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<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("RoleId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("RoleId")
.HasColumnType("uniqueidentifier");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("RefreshToken")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("RefreshTokenExpiryUtc")
.HasColumnType("datetime2");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("YandexAccessToken")
.HasColumnType("nvarchar(max)");
b.Property<string>("YandexId")
.HasColumnType("nvarchar(max)");
b.Property<string>("YandexRefreshToken")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("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<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("SharedPlaylistId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("AddedAtUtc")
.HasColumnType("datetime2");
b.HasKey("UserId", "SharedPlaylistId");
b.HasIndex("SharedPlaylistId");
b.ToTable("FavoritePlaylists");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("AddPermission")
.HasColumnType("int");
b.Property<string>("CoverUrl")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid>("CreatorUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<int>("PlayPermission")
.HasColumnType("int");
b.Property<int>("RemovePermission")
.HasColumnType("int");
b.Property<string>("ShareToken")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<int>("ViewPermission")
.HasColumnType("int");
b.Property<string>("YandexPlaylistKind")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("YandexPlaylistOwnerUid")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("AddedAtUtc")
.HasColumnType("datetime2");
b.Property<Guid?>("AddedByUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("SessionId")
.IsRequired()
.HasColumnType("nvarchar(449)");
b.Property<Guid>("SharedPlaylistId")
.HasColumnType("uniqueidentifier");
b.Property<string>("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<string>("SessionId")
.HasMaxLength(449)
.HasColumnType("nvarchar(449)");
b.Property<Guid?>("AssociatedUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("ClientIpAddress")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("FirstSeenUtc")
.HasColumnType("datetime2");
b.Property<DateTime>("LastSeenUtc")
.HasColumnType("datetime2");
b.Property<string>("UserAgent")
.HasColumnType("nvarchar(max)");
b.HasKey("SessionId");
b.HasIndex("AssociatedUserId");
b.ToTable("UserSessions");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.YandexAuthSession", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime?>("ConfirmedAt")
.HasColumnType("datetime2");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("CsfrToken")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("HeaderCsfrToken")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("HeaderProcessId")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<bool>("IsConfirmed")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<string>("QrCodeUrl")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("SerializedCookies")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("TrackId")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<Guid?>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("UserId")
.HasDatabaseName("IX_YandexAuthSessions_UserId");
b.ToTable("YandexAuthSessions");
});
modelBuilder.Entity("TrackRemovalLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("RemovedAtUtc")
.HasColumnType("datetime2");
b.Property<Guid?>("RemovedByUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("SessionId")
.IsRequired()
.HasColumnType("nvarchar(449)");
b.Property<Guid>("SharedPlaylistId")
.HasColumnType("uniqueidentifier");
b.Property<string>("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<System.Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", 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<System.Guid>", 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
}
}
}

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PlaylistShared.Api.Data.Migrations
{
/// <inheritdoc />
public partial class AddYandexAuthSessions_Header : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "HeaderCsfrToken",
table: "YandexAuthSessions",
type: "nvarchar(200)",
maxLength: 200,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "HeaderProcessId",
table: "YandexAuthSessions",
type: "nvarchar(200)",
maxLength: 200,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "HeaderCsfrToken",
table: "YandexAuthSessions");
migrationBuilder.DropColumn(
name: "HeaderProcessId",
table: "YandexAuthSessions");
}
}
}

View File

@@ -425,6 +425,14 @@ namespace PlaylistShared.Api.Data.Migrations
.HasMaxLength(200) .HasMaxLength(200)
.HasColumnType("nvarchar(200)"); .HasColumnType("nvarchar(200)");
b.Property<string>("HeaderCsfrToken")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("HeaderProcessId")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<bool>("IsConfirmed") b.Property<bool>("IsConfirmed")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("bit") .HasColumnType("bit")

View File

@@ -11,6 +11,8 @@ public class YandexAuthSession
public bool IsConfirmed { get; set; } public bool IsConfirmed { get; set; }
public string? TrackId { get; set; } public string? TrackId { get; set; }
public string? CsfrToken { get; set; } public string? CsfrToken { get; set; }
public string? HeaderProcessId { get; set; }
public string? HeaderCsfrToken { get; set; }
public ApplicationUser? User { get; set; } public ApplicationUser? User { get; set; }
} }

View File

@@ -27,7 +27,7 @@
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="10.1.7" /> <PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="10.1.7" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.7" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.7" />
<PackageReference Include="YandexMusic" Version="0.0.11" /> <PackageReference Include="YandexMusic" Version="0.0.14" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -2,7 +2,6 @@
using PlaylistShared.Api.Entities; using PlaylistShared.Api.Entities;
using System.Net; using System.Net;
using YandexMusic; using YandexMusic;
using YandexMusic.API.Common;
namespace PlaylistShared.Api.Services; namespace PlaylistShared.Api.Services;
@@ -12,7 +11,6 @@ namespace PlaylistShared.Api.Services;
public class YandexApiService : IDisposable public class YandexApiService : IDisposable
{ {
private readonly IDataProtector _dataProtector; private readonly IDataProtector _dataProtector;
private readonly HttpClient _httpClient;
private readonly YandexMusicClient _client; private readonly YandexMusicClient _client;
private readonly CookieContainer _cookieContainer; private readonly CookieContainer _cookieContainer;
@@ -33,12 +31,7 @@ public class YandexApiService : IDisposable
{ {
_dataProtector = provider.CreateProtector("YandexTokens"); _dataProtector = provider.CreateProtector("YandexTokens");
_cookieContainer = new(); _cookieContainer = new();
_httpClient = YandexMusicHttpClientFactory.CreateDefault( _client = new YandexMusicClient(_cookieContainer, proxy, timeout);
cookieContainer: _cookieContainer,
proxy: proxy,
timeout: timeout
);
_client = new YandexMusicClient(_httpClient);
} }
@@ -107,21 +100,6 @@ public class YandexApiService : IDisposable
return cookie?.Value; 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();
}
/// <summary> /// <summary>
/// Авторизуется с помощью OAuth-токена. /// Авторизуется с помощью OAuth-токена.
/// </summary> /// </summary>
@@ -133,6 +111,5 @@ public class YandexApiService : IDisposable
public void Dispose() public void Dispose()
{ {
_client.Dispose(); _client.Dispose();
_httpClient.Dispose();
} }
} }

View File

@@ -1,9 +1,9 @@
using PlaylistShared.Api.Data; using Microsoft.EntityFrameworkCore;
using PlaylistShared.Api.Data;
using PlaylistShared.Api.Entities; using PlaylistShared.Api.Entities;
using PlaylistShared.Shared.Yandex; using PlaylistShared.Shared.Yandex;
using System.Net; using System.Net;
using System.Text.Json; using System.Text.Json;
using YandexMusic.API.Models.Account;
namespace PlaylistShared.Api.Services; namespace PlaylistShared.Api.Services;
@@ -20,12 +20,33 @@ public class YandexAuthService
_dbContext = dbContext; _dbContext = dbContext;
} }
internal async Task<YandexAuthQr> 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<YandexAuthQr> GenerateQrAsync(ApplicationUser user) internal async Task<YandexAuthQr> GenerateQrAsync(ApplicationUser user)
{ {
var client = _apiService.Client; var client = _apiService.Client;
var qr = await client.GetAuthQRLink(); var qr = await client.GetAuthQRLink();
var trackId = client.AuthStorage.AuthToken.TrackId; var trackId = client.AuthStorage.AuthToken.TrackId;
var csfrToken = client.AuthStorage.AuthToken.CsfrToken; var csfrToken = client.AuthStorage.AuthToken.CsfrToken;
var headerProcessUuid = client.AuthStorage.HeaderToken.ProcessUuid;
var headerCsfrToken = client.AuthStorage.HeaderToken.CsfrToken;
if (string.IsNullOrEmpty(qr)) if (string.IsNullOrEmpty(qr))
throw new Exception("Не удалось получить QR-ссылку"); throw new Exception("Не удалось получить QR-ссылку");
@@ -41,6 +62,8 @@ public class YandexAuthService
IsConfirmed = false, IsConfirmed = false,
TrackId = trackId, TrackId = trackId,
CsfrToken = csfrToken, CsfrToken = csfrToken,
HeaderCsfrToken = headerCsfrToken,
HeaderProcessId = headerProcessUuid,
}; };
@@ -67,19 +90,26 @@ public class YandexAuthService
_apiService.Client.AuthStorage.AuthToken.CsfrToken = session?.CsfrToken ?? ""; _apiService.Client.AuthStorage.AuthToken.CsfrToken = session?.CsfrToken ?? "";
_apiService.Client.AuthStorage.AuthToken.TrackId = session?.TrackId ?? ""; _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; try
session.IsConfirmed = true;
await _dbContext.SaveChangesAsync();
return new()
{ {
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() return new()
@@ -91,7 +121,6 @@ public class YandexAuthService
private string SerializeCookies(CookieContainer container) private string SerializeCookies(CookieContainer container)
{ {
var domains = new[] { "yandex.ru", "passport.yandex.ru", ".yandex.ru" };
var allCookies = new List<object>(); var allCookies = new List<object>();
var cookies = container.GetAllCookies(); var cookies = container.GetAllCookies();
@@ -108,8 +137,7 @@ public class YandexAuthService
var cookies = JsonSerializer.Deserialize<List<CookieData>>(serializedCookies); var cookies = JsonSerializer.Deserialize<List<CookieData>>(serializedCookies);
foreach (var c in cookies) foreach (var c in cookies)
{ {
var uri = new Uri($"{c.Domain}"); container.Add(new Cookie(c.Name, c.Value, c.Path, c.Domain));
container.Add(uri, new Cookie(c.Name, c.Value, c.Path, c.Domain));
} }
} }

View File

@@ -88,7 +88,7 @@
{ {
try try
{ {
await Task.Delay(2000, token); await Task.Delay(500, token);
var statusResponse = await Http.GetFromJsonAsync<ApiResponse<YandexAuthQrCheck>>($"/api/yandexaccount/qr/{_sessionId}", token); var statusResponse = await Http.GetFromJsonAsync<ApiResponse<YandexAuthQrCheck>>($"/api/yandexaccount/qr/{_sessionId}", token);
if (statusResponse?.Data != null) if (statusResponse?.Data != null)
{ {
@@ -101,13 +101,15 @@
_statusText = "Авторизация успешна!"; _statusText = "Авторизация успешна!";
_isWaiting = false; _isWaiting = false;
Snackbar.Add("Авторизация выполнена", Severity.Success); Snackbar.Add("Авторизация выполнена", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
_cts?.Cancel(); _cts?.Cancel();
MudDialog.Close(DialogResult.Ok(true));
return; return;
case Shared.Enums.YandexAuthQrStatus.Expired: case Shared.Enums.YandexAuthQrStatus.Expired:
_cts?.Cancel();
ShowError("Срок действия QR-кода истёк"); ShowError("Срок действия QR-кода истёк");
return; return;
case Shared.Enums.YandexAuthQrStatus.Error: case Shared.Enums.YandexAuthQrStatus.Error:
_cts?.Cancel();
ShowError("Ошибка авторизации"); ShowError("Ошибка авторизации");
return; return;
} }

View File

@@ -39,7 +39,7 @@
Dense Dense
Color="Color.Primary"> Color="Color.Primary">
<MudMenuItem OnClick="OpenTokenDialog">Token</MudMenuItem> <MudMenuItem OnClick="OpenTokenDialog">Token</MudMenuItem>
<MudMenuItem OnClick="OpenQrDialog">Qr</MudMenuItem> <MudMenuItem OnClick="OpenQrServerDialog">QR</MudMenuItem>
</MudMenu> </MudMenu>
</MudStack> </MudStack>
</MudCardContent> </MudCardContent>
@@ -77,10 +77,10 @@
if (!result.Canceled) await LoadStatus(); 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 options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
var dialog = await DialogService.ShowAsync<YandexQrDialog>("", options); var dialog = await DialogService.ShowAsync<YandexQrServerDialog>("", options);
var result = await dialog.Result; var result = await dialog.Result;
if (!result.Canceled) await LoadStatus(); if (!result.Canceled) await LoadStatus();