Добавлен вывод QR яндекса

This commit is contained in:
FrigaT
2026-04-19 21:06:36 +03:00
parent 4324b86512
commit 12241639dc
21 changed files with 1349 additions and 46 deletions

View File

@@ -6,8 +6,8 @@ using PlaylistShared.Api.Extensions;
using PlaylistShared.Api.Services; using PlaylistShared.Api.Services;
using PlaylistShared.Shared; using PlaylistShared.Shared;
using PlaylistShared.Shared.Enums; using PlaylistShared.Shared.Enums;
using PlaylistShared.Shared.Yandex;
using PlaylistShared.Shared.SharedPlaylist; using PlaylistShared.Shared.SharedPlaylist;
using PlaylistShared.Shared.Yandex;
using YandexMusic; using YandexMusic;
namespace PlaylistShared.Api.Controllers; namespace PlaylistShared.Api.Controllers;
@@ -20,15 +20,18 @@ public class PlaylistsController : ControllerBase
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly SharedPlaylistService _sharedService; private readonly SharedPlaylistService _sharedService;
private readonly YandexMusicService _yandexService; private readonly YandexMusicService _yandexService;
private readonly YandexApiService _yandexApiService;
public PlaylistsController( public PlaylistsController(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
SharedPlaylistService sharedService, SharedPlaylistService sharedService,
YandexMusicService yandexService) YandexMusicService yandexService,
YandexApiService yandexApiService)
{ {
_userManager = userManager; _userManager = userManager;
_sharedService = sharedService; _sharedService = sharedService;
_yandexService = yandexService; _yandexService = yandexService;
_yandexApiService = yandexApiService;
} }
[HttpGet] [HttpGet]
@@ -38,7 +41,7 @@ public class PlaylistsController : 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 decryptedToken = _yandexService.DecryptToken(user.YandexAccessToken); var decryptedToken = _yandexApiService.DecryptToken(user.YandexAccessToken);
if (string.IsNullOrEmpty(decryptedToken)) if (string.IsNullOrEmpty(decryptedToken))
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 400, Message = "Токен Яндекс.Музыки не установлен или недействителен" })); return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 400, Message = "Токен Яндекс.Музыки не установлен или недействителен" }));
@@ -74,11 +77,9 @@ public class PlaylistsController : ControllerBase
if (user == null) return Unauthorized(); if (user == null) return Unauthorized();
// Проверяем, что плейлист действительно принадлежит пользователю // Проверяем, что плейлист действительно принадлежит пользователю
var yandexClient = new YandexMusicClient(); var playlist = await _yandexService.GetPlaylistAsync(user, request.OwnerUid, request.Kind);
await yandexClient.Authorize(_yandexService.DecryptToken(user.YandexAccessToken)); if (playlist == null)
var playlist = await yandexClient.GetPlaylistAsync(request.OwnerUid, request.Kind); return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" }));
if (playlist == null || playlist.Owner.Uid != yandexClient.Account.Uid)
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 400, Message = "Плейлист не принадлежит вам" }));
var dto = new SharePlaylistDto var dto = new SharePlaylistDto
{ {

View File

@@ -6,31 +6,32 @@ using PlaylistShared.Api.Extensions;
using PlaylistShared.Api.Services; using PlaylistShared.Api.Services;
using PlaylistShared.Shared; using PlaylistShared.Shared;
using PlaylistShared.Shared.Profile; using PlaylistShared.Shared.Profile;
using PlaylistShared.Shared.Yandex;
namespace PlaylistShared.Api.Controllers; namespace PlaylistShared.Api.Controllers;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
[Authorize] [Authorize]
public class YandexTokenController : ControllerBase public class YandexAccountController : ControllerBase
{ {
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly YandexMusicService _yandexService; private readonly YandexAuthService _yandexService;
public YandexTokenController(UserManager<ApplicationUser> userManager, YandexMusicService yandexService) public YandexAccountController(UserManager<ApplicationUser> userManager, YandexAuthService yandexService)
{ {
_userManager = userManager; _userManager = userManager;
_yandexService = yandexService; _yandexService = yandexService;
} }
[HttpPost("set")] [HttpPost("token")]
public async Task<ActionResult<ApiResponse<object>>> SetToken([FromBody] SetYandexTokenRequest request) public async Task<ActionResult<ApiResponse<object>>> SetToken([FromBody] SetYandexTokenRequest request)
{ {
var userId = User.GetUserId(); var userId = User.GetUserId();
var user = await _userManager.FindByIdAsync(userId.ToString()); var user = await _userManager.FindByIdAsync(userId.ToString());
if (user == null) return Unauthorized(); if (user == null) return Unauthorized();
user.YandexAccessToken = _yandexService.EncryptToken(request.Token); user.YandexAccessToken = _yandexService.Service.EncryptToken(request.Token);
// Не храним refresh-токен, так как пользователь вводит только access-токен // Не храним refresh-токен, так как пользователь вводит только access-токен
user.YandexTokenExpiryUtc = DateTime.UtcNow.AddMonths(1); // условно, т.к. срок жизни токена неизвестен user.YandexTokenExpiryUtc = DateTime.UtcNow.AddMonths(1); // условно, т.к. срок жизни токена неизвестен
await _userManager.UpdateAsync(user); await _userManager.UpdateAsync(user);
@@ -55,4 +56,35 @@ public class YandexTokenController : ControllerBase
ExpiryUtc = user.YandexTokenExpiryUtc ExpiryUtc = user.YandexTokenExpiryUtc
})); }));
} }
[HttpGet("qr")]
public async Task<ActionResult<ApiResponse<YandexAuthQr>>> GetQr()
{
var userId = User.GetUserId();
var user = await _userManager.FindByIdAsync(userId.ToString());
if (user == null) return Unauthorized();
var qr = await _yandexService.GenerateQrAsync(user);
return Ok(ApiResponse<YandexAuthQr>.Ok(qr));
}
[HttpGet("qr/{sessionId}")]
public async Task<IActionResult> CheckQr(int sessionId)
{
var userId = User.GetUserId();
var user = await _userManager.FindByIdAsync(userId.ToString());
if (user == null) return Unauthorized();
var checkResult = await _yandexService.CheckQrAsync(sessionId);
if (checkResult == null) return NotFound();
if (checkResult.Status == Shared.Enums.YandexAuthQrStatus.Authorized)
{
await SetToken(new() { Token = _yandexService.Service.Client.AuthStorage.Token });
}
return Ok(ApiResponse<YandexAuthQrCheck>.Ok(checkResult));
}
} }

View File

@@ -47,7 +47,7 @@ public class YandexSearchController : ControllerBase
user = await _userManager.FindByIdAsync(userId.Value.ToString()); user = await _userManager.FindByIdAsync(userId.Value.ToString());
// Если нет пользователя или у него нет токена, пробуем через shared_id // Если нет пользователя или у него нет токена, пробуем через shared_id
if (user == null || string.IsNullOrEmpty(_yandexService.DecryptToken(user.YandexAccessToken))) if (user == null || string.IsNullOrEmpty(user.YandexAccessToken))
{ {
if (string.IsNullOrEmpty(shared_id)) if (string.IsNullOrEmpty(shared_id))
return Unauthorized("Не установлен яндекс токен."); return Unauthorized("Не установлен яндекс токен.");
@@ -63,8 +63,7 @@ public class YandexSearchController : ControllerBase
user = owner; user = owner;
} }
var decryptedToken = _yandexService.DecryptToken(user.YandexAccessToken); if (string.IsNullOrEmpty(user.YandexAccessToken))
if (string.IsNullOrEmpty(decryptedToken))
return BadRequest(ApiResponse<YandexSearchResult>.Fail(new ErrorResponse return BadRequest(ApiResponse<YandexSearchResult>.Fail(new ErrorResponse
{ {
StatusCode = 400, StatusCode = 400,

View File

@@ -16,6 +16,7 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
public DbSet<TrackAdditionLog> TrackAdditionLogs => Set<TrackAdditionLog>(); public DbSet<TrackAdditionLog> TrackAdditionLogs => Set<TrackAdditionLog>();
public DbSet<TrackRemovalLog> TrackRemovalLogs => Set<TrackRemovalLog>(); public DbSet<TrackRemovalLog> TrackRemovalLogs => Set<TrackRemovalLog>();
public DbSet<UserSession> UserSessions => Set<UserSession>(); public DbSet<UserSession> UserSessions => Set<UserSession>();
public DbSet<YandexAuthSession> YandexAuthSessions => Set<YandexAuthSession>();
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; } public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
@@ -102,5 +103,36 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
entity.Property(e => e.AddedAtUtc).IsRequired(); entity.Property(e => e.AddedAtUtc).IsRequired();
}); });
builder.Entity<YandexAuthSession>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Id)
.ValueGeneratedOnAdd();
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
entity.Property(e => e.QrCodeUrl)
.IsRequired()
.HasMaxLength(500);
entity.Property(e => e.SerializedCookies)
.IsRequired()
.HasColumnType("nvarchar(max)");
entity.Property(e => e.ConfirmedAt)
.IsRequired(false);
entity.Property(e => e.IsConfirmed)
.IsRequired()
.HasDefaultValue(false);
entity.Property(e => e.TrackId)
.HasMaxLength(100)
.IsRequired(false);
entity.Property(e => e.CsfrToken)
.HasMaxLength(200)
.IsRequired(false);
entity.HasIndex(e => e.UserId)
.HasDatabaseName("IX_YandexAuthSessions_UserId");
});
} }
} }

View File

@@ -0,0 +1,668 @@
// <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("20260419180136_AddYandexAuthSessions")]
partial class AddYandexAuthSessions
{
/// <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<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,53 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PlaylistShared.Api.Data.Migrations
{
/// <inheritdoc />
public partial class AddYandexAuthSessions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "YandexAuthSessions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
QrCodeUrl = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
SerializedCookies = table.Column<string>(type: "nvarchar(max)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
ConfirmedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
IsConfirmed = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
TrackId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
CsfrToken = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_YandexAuthSessions", x => x.Id);
table.ForeignKey(
name: "FK_YandexAuthSessions_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateIndex(
name: "IX_YandexAuthSessions_UserId",
table: "YandexAuthSessions",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "YandexAuthSessions");
}
}
}

View File

@@ -17,7 +17,7 @@ namespace PlaylistShared.Api.Data.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "10.0.5") .HasAnnotation("ProductVersion", "10.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 128); .HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
@@ -407,6 +407,53 @@ namespace PlaylistShared.Api.Data.Migrations
b.ToTable("UserSessions"); 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<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 => modelBuilder.Entity("TrackRemovalLog", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@@ -558,6 +605,16 @@ namespace PlaylistShared.Api.Data.Migrations
b.Navigation("User"); 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 => modelBuilder.Entity("TrackRemovalLog", b =>
{ {
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "RemovedByUser") b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "RemovedByUser")

View File

@@ -12,4 +12,4 @@ public class UserSession
public ApplicationUser? User { get; set; } public ApplicationUser? User { get; set; }
public ICollection<TrackAdditionLog> TrackAdditionLogs { get; set; } = new List<TrackAdditionLog>(); public ICollection<TrackAdditionLog> TrackAdditionLogs { get; set; } = new List<TrackAdditionLog>();
public ICollection<TrackRemovalLog> TrackRemovalLogs { get; set; } = new List<TrackRemovalLog>(); public ICollection<TrackRemovalLog> TrackRemovalLogs { get; set; } = new List<TrackRemovalLog>();
} }

View File

@@ -0,0 +1,16 @@
namespace PlaylistShared.Api.Entities;
public class YandexAuthSession
{
public int Id { get; set; }
public Guid? UserId { get; set; }
public string QrCodeUrl { get; set; }
public string SerializedCookies { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ConfirmedAt { get; set; }
public bool IsConfirmed { get; set; }
public string? TrackId { get; set; }
public string? CsfrToken { 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.8" /> <PackageReference Include="YandexMusic" Version="0.0.11" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -92,14 +92,18 @@ public class Program
builder.Services.AddAuthorization(); builder.Services.AddAuthorization();
builder.Services.AddScoped<JwtService>(); builder.Services.AddScoped<JwtService>();
builder.Services.AddScoped<UserSessionService>(); builder.Services.AddScoped<UserSessionService>();
builder.Services.AddDataProtection()
.PersistKeysToDbContext<ApplicationDbContext>()
.SetApplicationName("PlaylistShared.Api");
builder.Services.AddScoped<YandexApiService>();
builder.Services.AddScoped<YandexMusicService>(); builder.Services.AddScoped<YandexMusicService>();
builder.Services.AddScoped<YandexAuthService>();
builder.Services.AddScoped<SharedPlaylistService>(); builder.Services.AddScoped<SharedPlaylistService>();
builder.Services.AddScoped<TrackAdditionLogService>(); builder.Services.AddScoped<TrackAdditionLogService>();
builder.Services.AddScoped<TrackRemovalLogService>(); builder.Services.AddScoped<TrackRemovalLogService>();
builder.Services.AddScoped<FavoritesService>(); builder.Services.AddScoped<FavoritesService>();
builder.Services.AddDataProtection()
.PersistKeysToDbContext<ApplicationDbContext>()
.SetApplicationName("PlaylistShared.Api");
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();

View File

@@ -0,0 +1,138 @@
using Microsoft.AspNetCore.DataProtection;
using PlaylistShared.Api.Entities;
using System.Net;
using YandexMusic;
using YandexMusic.API.Common;
namespace PlaylistShared.Api.Services;
/// <summary>
/// Сервис для работы с API Яндекс Музыки в ASP.NET Core.
/// </summary>
public class YandexApiService : IDisposable
{
private readonly IDataProtector _dataProtector;
private readonly HttpClient _httpClient;
private readonly YandexMusicClient _client;
private readonly CookieContainer _cookieContainer;
/// <summary>
/// Экземпляр клиента Яндекс Музыки.
/// </summary>
public YandexMusicClient Client => _client;
/// <summary>
/// Контейнер кук, используемый клиентом.
/// </summary>
public CookieContainer CookieContainer => _cookieContainer;
/// <summary>
/// Создаёт сервис с автоматическим созданием HttpClient (рекомендуется).
/// </summary>
public YandexApiService(IDataProtectionProvider provider, IWebProxy? proxy = null, TimeSpan? timeout = null)
{
_dataProtector = provider.CreateProtector("YandexTokens");
_cookieContainer = new();
_httpClient = YandexMusicHttpClientFactory.CreateDefault(
cookieContainer: _cookieContainer,
proxy: proxy,
timeout: timeout
);
_client = new YandexMusicClient(_httpClient);
}
public async Task<bool?> AuthAsync(ApplicationUser user)
{
if (string.IsNullOrEmpty(user.YandexAccessToken))
return null;
var decryptedToken = DecryptToken(user.YandexAccessToken);
if (decryptedToken == null)
return null;
return await _client.Authorize(decryptedToken);
}
/// <summary>
/// Засшифровывает и возвращает токен для хранения в базе данных.
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public string EncryptToken(string token) => _dataProtector.Protect(token);
/// <summary>
/// Расшифровывает ключ из базы данных. Если токен повреждён или недействителен, возвращает null.
/// </summary>
/// <param name="encryptedToken"></param>
/// <returns></returns>
public string DecryptToken(string encryptedToken)
{
try
{
return _dataProtector.Unprotect(encryptedToken);
}
catch
{
return null;
}
}
/// <summary>
/// Устанавливает куки из строки для указанного домена.
/// </summary>
public void SetCookies(string cookieString, string domain)
{
var uri = new Uri(domain.StartsWith("http") ? domain : $"https://{domain}");
_cookieContainer.SetCookies(uri, cookieString);
}
/// <summary>
/// Получает все куки для указанного домена в виде строки.
/// </summary>
public string GetCookies(string domain)
{
var uri = new Uri(domain.StartsWith("http") ? domain : $"https://{domain}");
var cookies = _cookieContainer.GetCookies(uri);
return string.Join("; ", cookies.Cast<Cookie>().Select(c => $"{c.Name}={c.Value}"));
}
/// <summary>
/// Получает значение конкретной куки.
/// </summary>
public string? GetCookie(string domain, string cookieName)
{
var uri = new Uri(domain.StartsWith("http") ? domain : $"https://{domain}");
var cookie = _cookieContainer.GetCookies(uri)[cookieName];
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>
/// Авторизуется с помощью OAuth-токена.
/// </summary>
public async Task<bool> AuthorizeAsync(string token)
{
return await _client.Authorize(token);
}
public void Dispose()
{
_client.Dispose();
_httpClient.Dispose();
}
}

View File

@@ -0,0 +1,123 @@
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;
public class YandexAuthService
{
private readonly YandexApiService _apiService;
private readonly ApplicationDbContext _dbContext;
public YandexApiService Service => _apiService;
public YandexAuthService(YandexApiService apiService, ApplicationDbContext dbContext)
{
_apiService = apiService;
_dbContext = dbContext;
}
internal async Task<YandexAuthQr> GenerateQrAsync(ApplicationUser user)
{
var client = _apiService.Client;
var qr = await client.GetAuthQRLink();
var trackId = client.AuthStorage.AuthToken.TrackId;
var csfrToken = client.AuthStorage.AuthToken.CsfrToken;
if (string.IsNullOrEmpty(qr))
throw new Exception("Не удалось получить QR-ссылку");
var cookiesJson = SerializeCookies(_apiService.CookieContainer);
var session = new YandexAuthSession
{
UserId = user.Id,
QrCodeUrl = qr,
SerializedCookies = cookiesJson,
CreatedAt = DateTime.UtcNow,
IsConfirmed = false,
TrackId = trackId,
CsfrToken = csfrToken,
};
_dbContext.YandexAuthSessions.Add(session);
await _dbContext.SaveChangesAsync();
return new YandexAuthQr
{
QrLink = qr,
SessionId = session.Id.ToString()
};
}
internal async Task<YandexAuthQrCheck?> CheckQrAsync(int sessionId)
{
var session = await _dbContext.YandexAuthSessions.FindAsync(sessionId);
if (session == null) return null;
RestoreCookies(_apiService.CookieContainer, session.SerializedCookies);
if (_apiService.Client.AuthStorage.AuthToken is null)
{
_apiService.Client.AuthStorage.AuthToken = new();
}
_apiService.Client.AuthStorage.AuthToken.CsfrToken = session?.CsfrToken ?? "";
_apiService.Client.AuthStorage.AuthToken.TrackId = session?.TrackId ?? "";
var status = await _apiService.Client.AuthorizeByQR();
if (status?.Status == YAuthStatus.Ok)
{
session.ConfirmedAt = DateTime.UtcNow;
session.IsConfirmed = true;
await _dbContext.SaveChangesAsync();
return new()
{
Status = Shared.Enums.YandexAuthQrStatus.Authorized,
};
}
return new()
{
Status = Shared.Enums.YandexAuthQrStatus.Pending,
};
}
private string SerializeCookies(CookieContainer container)
{
var domains = new[] { "yandex.ru", "passport.yandex.ru", ".yandex.ru" };
var allCookies = new List<object>();
var cookies = container.GetAllCookies();
foreach (Cookie cookie in cookies)
{
allCookies.Add(new { cookie.Name, cookie.Value, cookie.Domain, cookie.Path });
}
return JsonSerializer.Serialize(allCookies);
}
private void RestoreCookies(CookieContainer container, string serializedCookies)
{
var cookies = JsonSerializer.Deserialize<List<CookieData>>(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));
}
}
private class CookieData
{
public string Name { get; set; }
public string Value { get; set; }
public string Domain { get; set; }
public string Path { get; set; }
}
}

View File

@@ -95,20 +95,6 @@ public class YandexMusicService
return track; return track;
} }
public string EncryptToken(string token) => _dataProtector.Protect(token);
public string DecryptToken(string encryptedToken)
{
try
{
return _dataProtector.Unprotect(encryptedToken);
}
catch
{
return null;
}
}
public async Task<YandexSearchResult> SearchAsync( public async Task<YandexSearchResult> SearchAsync(
ApplicationUser user, ApplicationUser user,
string query, string query,

View File

@@ -0,0 +1,145 @@
@using System.Threading
@using PlaylistShared.Shared.DTO
@using PlaylistShared.Shared.Yandex
@inject HttpClient Http
@inject ISnackbar Snackbar
@inject IJSRuntime JsRuntime
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">Авторизация Яндекс.Музыки по QR</MudText>
</TitleContent>
<DialogContent>
@if (_qrUrl != null)
{
<div style="text-align: center;">
<MudText Typo="Typo.body2" Class="mb-2">Отсканируйте QR-код приложением Яндекс</MudText>
<MudImage Src="@_qrUrl" Style="max-width: 250px; border-radius: 12px; background-color: white;" />
<MudText Typo="Typo.body2" Class="mt-2" Color="Color.Secondary">
Статус: @_statusText
</MudText>
@if (_isWaiting)
{
<MudProgressCircular Indeterminate Class="mt-2" Size="Size.Small" />
}
@if (_isError)
{
<MudAlert Severity="Severity.Error" Class="mt-4">
@_errorMessage
</MudAlert>
}
</div>
}
else
{
<MudProgressCircular Indeterminate />
}
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Text" OnClick="Cancel">Отмена</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
private string _qrUrl;
private string _sessionId;
private string _statusText = "Ожидание сканирования";
private bool _isWaiting = true;
private bool _isError = false;
private string _errorMessage = "";
private CancellationTokenSource _cts;
protected override async Task OnInitializedAsync()
{
await StartQrFlow();
}
private async Task StartQrFlow()
{
try
{
// 1. Получить QR и sessionId
var response = await Http.GetFromJsonAsync<ApiResponse<YandexAuthQr>>("/api/yandexaccount/qr");
if (!response.Success || response.Data == null)
{
ShowError("Не удалось получить QR-код");
return;
}
_qrUrl = response.Data.QrLink;
_sessionId = response.Data.SessionId;
// 2. Начать опрос статуса
_cts = new CancellationTokenSource();
_ = PollStatus(_cts.Token);
StateHasChanged();
}
catch (Exception ex)
{
ShowError(ex.Message);
}
}
private async Task PollStatus(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
await Task.Delay(2000, token);
var statusResponse = await Http.GetFromJsonAsync<ApiResponse<YandexAuthQrCheck>>($"/api/yandexaccount/qr/{_sessionId}", token);
if (statusResponse?.Data != null)
{
switch (statusResponse.Data.Status)
{
case Shared.Enums.YandexAuthQrStatus.Pending:
_statusText = "Ожидание подтверждения...";
break;
case Shared.Enums.YandexAuthQrStatus.Authorized:
_statusText = "Авторизация успешна!";
_isWaiting = false;
Snackbar.Add("Авторизация выполнена", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
_cts?.Cancel();
return;
case Shared.Enums.YandexAuthQrStatus.Expired:
ShowError("Срок действия QR-кода истёк");
return;
case Shared.Enums.YandexAuthQrStatus.Error:
ShowError("Ошибка авторизации");
return;
}
StateHasChanged();
}
}
catch (TaskCanceledException) { break; }
catch (Exception ex)
{
ShowError(ex.Message);
break;
}
}
}
private void ShowError(string message)
{
_isError = true;
_errorMessage = message;
_isWaiting = false;
StateHasChanged();
}
private void Cancel()
{
_cts?.Cancel();
MudDialog.Close(DialogResult.Cancel());
}
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
}
}

View File

@@ -74,7 +74,7 @@
_tokenErr = false; _tokenErr = false;
var response = await Http.PostAsJsonAsync("/api/yandextoken/set", new SetYandexTokenRequest { Token = token }); var response = await Http.PostAsJsonAsync("/api/yandexaccount/token", new SetYandexTokenRequest { Token = token });
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
Snackbar.Add("Токен успешно обновлен", Severity.Success); Snackbar.Add("Токен успешно обновлен", Severity.Success);

View File

@@ -2,7 +2,7 @@
@attribute [Authorize] @attribute [Authorize]
@inject HttpClient Http @inject HttpClient Http
@inject IDialogService DialogService @inject IDialogService DialogService
@using PlaylistShared.Pwa.Components.Profile @using PlaylistShared.Pwa.Components.Profile.YandexAccount
@using PlaylistShared.Shared.Profile @using PlaylistShared.Shared.Profile
<PageTitle>Профиль</PageTitle> <PageTitle>Профиль</PageTitle>
@@ -32,11 +32,15 @@
@_statusText @_statusText
</MudText> </MudText>
</MudStack> </MudStack>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary" <MudMenu EndIcon="@Icons.Material.Filled.ArrowDropDown"
OnClick="OpenTokenDialog"> Label="@(_hasToken ? "Переподключить" : "Подключить")"
@(_hasToken ? "Переподключить" : "Установить") Variant="Variant.Outlined"
</MudButton> Dense
Color="Color.Primary">
<MudMenuItem OnClick="OpenTokenDialog">Token</MudMenuItem>
<MudMenuItem OnClick="OpenQrDialog">Qr</MudMenuItem>
</MudMenu>
</MudStack> </MudStack>
</MudCardContent> </MudCardContent>
</MudCard> </MudCard>
@@ -44,7 +48,7 @@
</MudContainer> </MudContainer>
@code { @code {
private string _email = "user@example.com"; // Загрузите из стейта или API private string _email = "user@example.com";
private string _statusText = "Загрузка..."; private string _statusText = "Загрузка...";
private bool _hasToken; private bool _hasToken;
@@ -54,7 +58,7 @@
{ {
try try
{ {
var response = await Http.GetFromJsonAsync<ApiResponse<YandexTokenStatus>>("/api/yandextoken/status"); var response = await Http.GetFromJsonAsync<ApiResponse<YandexTokenStatus>>("/api/yandexaccount/status");
if (response?.Success == true) if (response?.Success == true)
{ {
_hasToken = response.Data.HasToken; _hasToken = response.Data.HasToken;
@@ -72,4 +76,13 @@
if (!result.Canceled) await LoadStatus(); if (!result.Canceled) await LoadStatus();
} }
private async Task OpenQrDialog()
{
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
var dialog = await DialogService.ShowAsync<YandexQrDialog>("", options);
var result = await dialog.Result;
if (!result.Canceled) await LoadStatus();
}
} }

View File

@@ -13,4 +13,4 @@ public enum TrackSearchType
Album, Album,
Playlist, Playlist,
Track, Track,
} }

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace PlaylistShared.Shared.Enums;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum YandexAuthQrStatus
{
Pending,
Authorized,
Expired,
Error,
}

View File

@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace PlaylistShared.Shared.Yandex;
/// <summary>Результат авторизации QR</summary>
public class YandexAuthQr
{
[JsonPropertyName("qrLink")]
public string QrLink { get; set; } = string.Empty;
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,11 @@
using PlaylistShared.Shared.Enums;
using System.Text.Json.Serialization;
namespace PlaylistShared.Shared.Yandex;
/// <summary>Результат авторизации QR</summary>
public class YandexAuthQrCheck
{
[JsonPropertyName("status")]
public YandexAuthQrStatus Status { get; set; } = YandexAuthQrStatus.Pending;
}