diff --git a/PlaylistShared.Api/Controllers/PlaylistsController.cs b/PlaylistShared.Api/Controllers/PlaylistsController.cs index 9b74e49..4284c62 100644 --- a/PlaylistShared.Api/Controllers/PlaylistsController.cs +++ b/PlaylistShared.Api/Controllers/PlaylistsController.cs @@ -6,8 +6,8 @@ using PlaylistShared.Api.Extensions; using PlaylistShared.Api.Services; using PlaylistShared.Shared; using PlaylistShared.Shared.Enums; -using PlaylistShared.Shared.Yandex; using PlaylistShared.Shared.SharedPlaylist; +using PlaylistShared.Shared.Yandex; using YandexMusic; namespace PlaylistShared.Api.Controllers; @@ -20,15 +20,18 @@ public class PlaylistsController : ControllerBase private readonly UserManager _userManager; private readonly SharedPlaylistService _sharedService; private readonly YandexMusicService _yandexService; + private readonly YandexApiService _yandexApiService; public PlaylistsController( UserManager userManager, SharedPlaylistService sharedService, - YandexMusicService yandexService) + YandexMusicService yandexService, + YandexApiService yandexApiService) { _userManager = userManager; _sharedService = sharedService; _yandexService = yandexService; + _yandexApiService = yandexApiService; } [HttpGet] @@ -38,7 +41,7 @@ public class PlaylistsController : ControllerBase var user = await _userManager.FindByIdAsync(userId.ToString()); if (user == null) return Unauthorized(); - var decryptedToken = _yandexService.DecryptToken(user.YandexAccessToken); + var decryptedToken = _yandexApiService.DecryptToken(user.YandexAccessToken); if (string.IsNullOrEmpty(decryptedToken)) return BadRequest(ApiResponse.Fail(new ErrorResponse { StatusCode = 400, Message = "Токен Яндекс.Музыки не установлен или недействителен" })); @@ -74,11 +77,9 @@ public class PlaylistsController : ControllerBase if (user == null) return Unauthorized(); // Проверяем, что плейлист действительно принадлежит пользователю - var yandexClient = new YandexMusicClient(); - await yandexClient.Authorize(_yandexService.DecryptToken(user.YandexAccessToken)); - var playlist = await yandexClient.GetPlaylistAsync(request.OwnerUid, request.Kind); - if (playlist == null || playlist.Owner.Uid != yandexClient.Account.Uid) - return BadRequest(ApiResponse.Fail(new ErrorResponse { StatusCode = 400, Message = "Плейлист не принадлежит вам" })); + var playlist = await _yandexService.GetPlaylistAsync(user, request.OwnerUid, request.Kind); + if (playlist == null) + return BadRequest(ApiResponse.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" })); var dto = new SharePlaylistDto { diff --git a/PlaylistShared.Api/Controllers/YandexTokenController.cs b/PlaylistShared.Api/Controllers/YandexAccountController.cs similarity index 57% rename from PlaylistShared.Api/Controllers/YandexTokenController.cs rename to PlaylistShared.Api/Controllers/YandexAccountController.cs index 96aecf4..7ae730e 100644 --- a/PlaylistShared.Api/Controllers/YandexTokenController.cs +++ b/PlaylistShared.Api/Controllers/YandexAccountController.cs @@ -6,31 +6,32 @@ using PlaylistShared.Api.Extensions; using PlaylistShared.Api.Services; using PlaylistShared.Shared; using PlaylistShared.Shared.Profile; +using PlaylistShared.Shared.Yandex; namespace PlaylistShared.Api.Controllers; [ApiController] [Route("api/[controller]")] [Authorize] -public class YandexTokenController : ControllerBase +public class YandexAccountController : ControllerBase { private readonly UserManager _userManager; - private readonly YandexMusicService _yandexService; + private readonly YandexAuthService _yandexService; - public YandexTokenController(UserManager userManager, YandexMusicService yandexService) + public YandexAccountController(UserManager userManager, YandexAuthService yandexService) { _userManager = userManager; _yandexService = yandexService; } - [HttpPost("set")] + [HttpPost("token")] public async Task>> SetToken([FromBody] SetYandexTokenRequest request) { var userId = User.GetUserId(); var user = await _userManager.FindByIdAsync(userId.ToString()); if (user == null) return Unauthorized(); - user.YandexAccessToken = _yandexService.EncryptToken(request.Token); + user.YandexAccessToken = _yandexService.Service.EncryptToken(request.Token); // Не храним refresh-токен, так как пользователь вводит только access-токен user.YandexTokenExpiryUtc = DateTime.UtcNow.AddMonths(1); // условно, т.к. срок жизни токена неизвестен await _userManager.UpdateAsync(user); @@ -55,4 +56,35 @@ public class YandexTokenController : ControllerBase ExpiryUtc = user.YandexTokenExpiryUtc })); } + + [HttpGet("qr")] + public async Task>> 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.Ok(qr)); + } + + [HttpGet("qr/{sessionId}")] + public async Task 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.Ok(checkResult)); + } } \ No newline at end of file diff --git a/PlaylistShared.Api/Controllers/YandexSearchController.cs b/PlaylistShared.Api/Controllers/YandexSearchController.cs index 878b725..ff3511c 100644 --- a/PlaylistShared.Api/Controllers/YandexSearchController.cs +++ b/PlaylistShared.Api/Controllers/YandexSearchController.cs @@ -47,7 +47,7 @@ public class YandexSearchController : ControllerBase user = await _userManager.FindByIdAsync(userId.Value.ToString()); // Если нет пользователя или у него нет токена, пробуем через shared_id - if (user == null || string.IsNullOrEmpty(_yandexService.DecryptToken(user.YandexAccessToken))) + if (user == null || string.IsNullOrEmpty(user.YandexAccessToken)) { if (string.IsNullOrEmpty(shared_id)) return Unauthorized("Не установлен яндекс токен."); @@ -63,8 +63,7 @@ public class YandexSearchController : ControllerBase user = owner; } - var decryptedToken = _yandexService.DecryptToken(user.YandexAccessToken); - if (string.IsNullOrEmpty(decryptedToken)) + if (string.IsNullOrEmpty(user.YandexAccessToken)) return BadRequest(ApiResponse.Fail(new ErrorResponse { StatusCode = 400, diff --git a/PlaylistShared.Api/Data/ApplicationDbContext.cs b/PlaylistShared.Api/Data/ApplicationDbContext.cs index 9967663..cf57941 100644 --- a/PlaylistShared.Api/Data/ApplicationDbContext.cs +++ b/PlaylistShared.Api/Data/ApplicationDbContext.cs @@ -16,6 +16,7 @@ public class ApplicationDbContext : IdentityDbContext TrackAdditionLogs => Set(); public DbSet TrackRemovalLogs => Set(); public DbSet UserSessions => Set(); + public DbSet YandexAuthSessions => Set(); public DbSet DataProtectionKeys { get; set; } protected override void OnModelCreating(ModelBuilder builder) @@ -102,5 +103,36 @@ public class ApplicationDbContext : IdentityDbContext e.AddedAtUtc).IsRequired(); }); + + + builder.Entity(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"); + }); } } \ No newline at end of file diff --git a/PlaylistShared.Api/Data/Migrations/20260419180136_AddYandexAuthSessions.Designer.cs b/PlaylistShared.Api/Data/Migrations/20260419180136_AddYandexAuthSessions.Designer.cs new file mode 100644 index 0000000..1f9e9c3 --- /dev/null +++ b/PlaylistShared.Api/Data/Migrations/20260419180136_AddYandexAuthSessions.Designer.cs @@ -0,0 +1,668 @@ +// +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 + { + /// + 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("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/20260419180136_AddYandexAuthSessions.cs b/PlaylistShared.Api/Data/Migrations/20260419180136_AddYandexAuthSessions.cs new file mode 100644 index 0000000..fd94059 --- /dev/null +++ b/PlaylistShared.Api/Data/Migrations/20260419180136_AddYandexAuthSessions.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PlaylistShared.Api.Data.Migrations +{ + /// + public partial class AddYandexAuthSessions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "YandexAuthSessions", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "uniqueidentifier", nullable: true), + QrCodeUrl = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + SerializedCookies = table.Column(type: "nvarchar(max)", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + ConfirmedAt = table.Column(type: "datetime2", nullable: true), + IsConfirmed = table.Column(type: "bit", nullable: false, defaultValue: false), + TrackId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + CsfrToken = table.Column(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"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "YandexAuthSessions"); + } + } +} diff --git a/PlaylistShared.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/PlaylistShared.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs index a67d022..dba31a1 100644 --- a/PlaylistShared.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/PlaylistShared.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace PlaylistShared.Api.Data.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("ProductVersion", "10.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -407,6 +407,53 @@ namespace PlaylistShared.Api.Data.Migrations 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("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") @@ -558,6 +605,16 @@ namespace PlaylistShared.Api.Data.Migrations 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") diff --git a/PlaylistShared.Api/Entities/UserSession.cs b/PlaylistShared.Api/Entities/UserSession.cs index dc41e76..aa16e63 100644 --- a/PlaylistShared.Api/Entities/UserSession.cs +++ b/PlaylistShared.Api/Entities/UserSession.cs @@ -12,4 +12,4 @@ public class UserSession public ApplicationUser? User { get; set; } public ICollection TrackAdditionLogs { get; set; } = new List(); public ICollection TrackRemovalLogs { get; set; } = new List(); -} \ No newline at end of file +} diff --git a/PlaylistShared.Api/Entities/YandexAuthSession.cs b/PlaylistShared.Api/Entities/YandexAuthSession.cs new file mode 100644 index 0000000..ccf5345 --- /dev/null +++ b/PlaylistShared.Api/Entities/YandexAuthSession.cs @@ -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; } +} \ No newline at end of file diff --git a/PlaylistShared.Api/PlaylistShared.Api.csproj b/PlaylistShared.Api/PlaylistShared.Api.csproj index 3e81a02..cc00d48 100644 --- a/PlaylistShared.Api/PlaylistShared.Api.csproj +++ b/PlaylistShared.Api/PlaylistShared.Api.csproj @@ -27,7 +27,7 @@ - + diff --git a/PlaylistShared.Api/Program.cs b/PlaylistShared.Api/Program.cs index 3f2eb80..45ea2e0 100644 --- a/PlaylistShared.Api/Program.cs +++ b/PlaylistShared.Api/Program.cs @@ -92,14 +92,18 @@ public class Program builder.Services.AddAuthorization(); builder.Services.AddScoped(); builder.Services.AddScoped(); + + builder.Services.AddDataProtection() + .PersistKeysToDbContext() + .SetApplicationName("PlaylistShared.Api"); + + builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); - builder.Services.AddDataProtection() - .PersistKeysToDbContext() - .SetApplicationName("PlaylistShared.Api"); builder.Services.AddHttpClient(); diff --git a/PlaylistShared.Api/Services/Yandex/YandexApiService.cs b/PlaylistShared.Api/Services/Yandex/YandexApiService.cs new file mode 100644 index 0000000..4c0c971 --- /dev/null +++ b/PlaylistShared.Api/Services/Yandex/YandexApiService.cs @@ -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; + +/// +/// Сервис для работы с API Яндекс Музыки в ASP.NET Core. +/// +public class YandexApiService : IDisposable +{ + private readonly IDataProtector _dataProtector; + private readonly HttpClient _httpClient; + private readonly YandexMusicClient _client; + private readonly CookieContainer _cookieContainer; + + /// + /// Экземпляр клиента Яндекс Музыки. + /// + public YandexMusicClient Client => _client; + + /// + /// Контейнер кук, используемый клиентом. + /// + public CookieContainer CookieContainer => _cookieContainer; + + /// + /// Создаёт сервис с автоматическим созданием HttpClient (рекомендуется). + /// + 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 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); + } + + /// + /// Засшифровывает и возвращает токен для хранения в базе данных. + /// + /// + /// + public string EncryptToken(string token) => _dataProtector.Protect(token); + + /// + /// Расшифровывает ключ из базы данных. Если токен повреждён или недействителен, возвращает null. + /// + /// + /// + public string DecryptToken(string encryptedToken) + { + try + { + return _dataProtector.Unprotect(encryptedToken); + } + catch + { + return null; + } + } + + /// + /// Устанавливает куки из строки для указанного домена. + /// + public void SetCookies(string cookieString, string domain) + { + var uri = new Uri(domain.StartsWith("http") ? domain : $"https://{domain}"); + _cookieContainer.SetCookies(uri, cookieString); + } + + /// + /// Получает все куки для указанного домена в виде строки. + /// + 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().Select(c => $"{c.Name}={c.Value}")); + } + + /// + /// Получает значение конкретной куки. + /// + 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(); + } + + /// + /// Авторизуется с помощью OAuth-токена. + /// + public async Task AuthorizeAsync(string token) + { + return await _client.Authorize(token); + } + + 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 new file mode 100644 index 0000000..7a91d06 --- /dev/null +++ b/PlaylistShared.Api/Services/Yandex/YandexAuthService.cs @@ -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 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 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(); + + 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>(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; } + } +} \ No newline at end of file diff --git a/PlaylistShared.Api/Services/YandexMusicService.cs b/PlaylistShared.Api/Services/Yandex/YandexMusicService.cs similarity index 97% rename from PlaylistShared.Api/Services/YandexMusicService.cs rename to PlaylistShared.Api/Services/Yandex/YandexMusicService.cs index 0a6333c..31308a5 100644 --- a/PlaylistShared.Api/Services/YandexMusicService.cs +++ b/PlaylistShared.Api/Services/Yandex/YandexMusicService.cs @@ -95,20 +95,6 @@ public class YandexMusicService 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 SearchAsync( ApplicationUser user, string query, diff --git a/PlaylistShared.Pwa/Components/Profile/YandexAccount/YandexQrDialog.razor b/PlaylistShared.Pwa/Components/Profile/YandexAccount/YandexQrDialog.razor new file mode 100644 index 0000000..34bac82 --- /dev/null +++ b/PlaylistShared.Pwa/Components/Profile/YandexAccount/YandexQrDialog.razor @@ -0,0 +1,145 @@ +@using System.Threading +@using PlaylistShared.Shared.DTO +@using PlaylistShared.Shared.Yandex +@inject HttpClient Http +@inject ISnackbar Snackbar +@inject IJSRuntime JsRuntime + + + + Авторизация Яндекс.Музыки по QR + + + @if (_qrUrl != null) + { +
+ Отсканируйте QR-код приложением Яндекс + + + Статус: @_statusText + + @if (_isWaiting) + { + + } + @if (_isError) + { + + @_errorMessage + + } +
+ } + else + { + + } +
+ + Отмена + +
+ +@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>("/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>($"/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(); + } +} \ No newline at end of file diff --git a/PlaylistShared.Pwa/Components/Profile/YandexTokenDialog.razor b/PlaylistShared.Pwa/Components/Profile/YandexAccount/YandexTokenDialog.razor similarity index 97% rename from PlaylistShared.Pwa/Components/Profile/YandexTokenDialog.razor rename to PlaylistShared.Pwa/Components/Profile/YandexAccount/YandexTokenDialog.razor index 9a1184a..cbc1c43 100644 --- a/PlaylistShared.Pwa/Components/Profile/YandexTokenDialog.razor +++ b/PlaylistShared.Pwa/Components/Profile/YandexAccount/YandexTokenDialog.razor @@ -74,7 +74,7 @@ _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) { Snackbar.Add("Токен успешно обновлен", Severity.Success); diff --git a/PlaylistShared.Pwa/Pages/Profile.razor b/PlaylistShared.Pwa/Pages/Profile.razor index 22a5a1d..6b23252 100644 --- a/PlaylistShared.Pwa/Pages/Profile.razor +++ b/PlaylistShared.Pwa/Pages/Profile.razor @@ -2,7 +2,7 @@ @attribute [Authorize] @inject HttpClient Http @inject IDialogService DialogService -@using PlaylistShared.Pwa.Components.Profile +@using PlaylistShared.Pwa.Components.Profile.YandexAccount @using PlaylistShared.Shared.Profile Профиль @@ -32,11 +32,15 @@ @_statusText - - @(_hasToken ? "Переподключить" : "Установить") - + + + Token + Qr + @@ -44,7 +48,7 @@ @code { - private string _email = "user@example.com"; // Загрузите из стейта или API + private string _email = "user@example.com"; private string _statusText = "Загрузка..."; private bool _hasToken; @@ -54,7 +58,7 @@ { try { - var response = await Http.GetFromJsonAsync>("/api/yandextoken/status"); + var response = await Http.GetFromJsonAsync>("/api/yandexaccount/status"); if (response?.Success == true) { _hasToken = response.Data.HasToken; @@ -72,4 +76,13 @@ 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("", options); + var result = await dialog.Result; + + if (!result.Canceled) await LoadStatus(); + } } diff --git a/PlaylistShared.Shared/Enums/TrackSearchType.cs b/PlaylistShared.Shared/Enums/TrackSearchType.cs index f4999c8..90e796a 100644 --- a/PlaylistShared.Shared/Enums/TrackSearchType.cs +++ b/PlaylistShared.Shared/Enums/TrackSearchType.cs @@ -13,4 +13,4 @@ public enum TrackSearchType Album, Playlist, Track, -} \ No newline at end of file +} diff --git a/PlaylistShared.Shared/Enums/YandexAuthQrStatus.cs b/PlaylistShared.Shared/Enums/YandexAuthQrStatus.cs new file mode 100644 index 0000000..e4caf9c --- /dev/null +++ b/PlaylistShared.Shared/Enums/YandexAuthQrStatus.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace PlaylistShared.Shared.Enums; + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum YandexAuthQrStatus +{ + Pending, + Authorized, + Expired, + Error, +} \ No newline at end of file diff --git a/PlaylistShared.Shared/Yandex/YandexAuthQr.cs b/PlaylistShared.Shared/Yandex/YandexAuthQr.cs new file mode 100644 index 0000000..9c9e0d5 --- /dev/null +++ b/PlaylistShared.Shared/Yandex/YandexAuthQr.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace PlaylistShared.Shared.Yandex; + +/// Результат авторизации QR +public class YandexAuthQr +{ + [JsonPropertyName("qrLink")] + public string QrLink { get; set; } = string.Empty; + + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/PlaylistShared.Shared/Yandex/YandexAuthQrCheck.cs b/PlaylistShared.Shared/Yandex/YandexAuthQrCheck.cs new file mode 100644 index 0000000..4da14a3 --- /dev/null +++ b/PlaylistShared.Shared/Yandex/YandexAuthQrCheck.cs @@ -0,0 +1,11 @@ +using PlaylistShared.Shared.Enums; +using System.Text.Json.Serialization; + +namespace PlaylistShared.Shared.Yandex; + +/// Результат авторизации QR +public class YandexAuthQrCheck +{ + [JsonPropertyName("status")] + public YandexAuthQrStatus Status { get; set; } = YandexAuthQrStatus.Pending; +}