Проведен аудит. Добавлено переключение треков
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PlaylistShared.Api.Entities;
|
||||
using PlaylistShared.Api.Services;
|
||||
using PlaylistShared.Shared;
|
||||
using PlaylistShared.Shared.Auth;
|
||||
using PlaylistShared.Shared.DTO;
|
||||
|
||||
namespace PlaylistShared.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AccountController : ControllerBase
|
||||
@@ -72,7 +75,7 @@ public class AccountController : ControllerBase
|
||||
[HttpPost("refresh-token")]
|
||||
public async Task<ActionResult<ApiResponse<LoginResponse>>> RefreshToken([FromBody] RefreshTokenRequest request)
|
||||
{
|
||||
var user = _userManager.Users.FirstOrDefault(u => u.RefreshToken == request.RefreshToken && u.RefreshTokenExpiryUtc > DateTime.UtcNow);
|
||||
var user = await _userManager.Users.FirstOrDefaultAsync(u => u.RefreshToken == request.RefreshToken && u.RefreshTokenExpiryUtc > DateTime.UtcNow);
|
||||
if (user == null)
|
||||
return Unauthorized(ApiResponse<LoginResponse>.Fail(new ErrorResponse { StatusCode = 401, Message = "Неверный или просроченный refresh token" }));
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlaylistShared.Api.Entities;
|
||||
@@ -18,47 +18,50 @@ public class AudioController : ControllerBase
|
||||
private readonly YandexMusicService _yandexService;
|
||||
private readonly SharedPlaylistService _sharedService;
|
||||
private readonly JwtService _jwtService;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public AudioController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
YandexMusicService yandexService,
|
||||
SharedPlaylistService sharedService,
|
||||
JwtService jwtService)
|
||||
JwtService jwtService,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_yandexService = yandexService;
|
||||
_sharedService = sharedService;
|
||||
_jwtService = jwtService;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
[HttpGet("play-token")]
|
||||
[Authorize]
|
||||
public IActionResult GetPlayToken()
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
var token = _jwtService.CreatePlayToken(userId);
|
||||
return Ok(ApiResponse<string>.Ok(token));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Потоковое воспроизведение трека из Яндекс.Музыки.
|
||||
/// </summary>
|
||||
/// <param name="trackId">ID трека (например, "21696942").</param>
|
||||
/// <param name="access_token">gwt пользователя</param>
|
||||
/// <param name="shared_id">ID расшаренного плейлиста</param>
|
||||
[HttpGet("track/{trackId}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> StreamTrack(string trackId, [FromQuery] string? access_token = null, [FromQuery] string? shared_id = null)
|
||||
public async Task<IActionResult> StreamTrack(string trackId, [FromQuery] string? play_token = null, [FromQuery] string? shared_id = null)
|
||||
{
|
||||
var user = await GetUserFromToken(access_token);
|
||||
var user = await GetUserFromPlayToken(play_token);
|
||||
if (user == null || user.YandexAccessToken is null) user = await GetUserFromSharedPlaylistId(shared_id);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var streamUrl = await _yandexService.GetTrackFileUrlAsync(user, trackId);
|
||||
if (string.IsNullOrEmpty(streamUrl)) return NotFound();
|
||||
|
||||
var httpClient = new HttpClient();
|
||||
var httpClient = _httpClientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, streamUrl);
|
||||
|
||||
// Пробрасываем Range-заголовок клиента к Яндекс.Музыке
|
||||
if (Request.Headers.ContainsKey("Range"))
|
||||
{
|
||||
request.Headers.Add("Range", Request.Headers["Range"].ToString());
|
||||
}
|
||||
|
||||
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
// Если Яндекс.Музыка поддерживает range, пробрасываем статус 206
|
||||
Response.StatusCode = (int)response.StatusCode;
|
||||
Response.ContentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg";
|
||||
|
||||
@@ -75,9 +78,9 @@ public class AudioController : ControllerBase
|
||||
|
||||
[HttpGet("track-info/{trackId}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<ApiResponse<YandexTrack>>> GetTrackInfo(string trackId, [FromQuery] string? access_token = null, [FromQuery] string? shared_id = null)
|
||||
public async Task<ActionResult<ApiResponse<YandexTrack>>> GetTrackInfo(string trackId, [FromQuery] string? play_token = null, [FromQuery] string? shared_id = null)
|
||||
{
|
||||
var user = await GetUserFromToken(access_token);
|
||||
var user = await GetUserFromPlayToken(play_token);
|
||||
if (user == null || user.YandexAccessToken is null) user = await GetUserFromSharedPlaylistId(shared_id);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
@@ -99,30 +102,20 @@ public class AudioController : ControllerBase
|
||||
}));
|
||||
}
|
||||
|
||||
private async Task<ApplicationUser?> GetUserFromToken(string? token)
|
||||
private async Task<ApplicationUser?> GetUserFromPlayToken(string? token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(token)) return null;
|
||||
|
||||
var principal = _jwtService.ValidateToken(token);
|
||||
if (principal == null) return null;
|
||||
|
||||
var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (string.IsNullOrEmpty(userId)) return null;
|
||||
|
||||
return await _userManager.FindByIdAsync(userId);
|
||||
var userId = _jwtService.ValidatePlayToken(token);
|
||||
if (!userId.HasValue) return null;
|
||||
return await _userManager.FindByIdAsync(userId.Value.ToString());
|
||||
}
|
||||
|
||||
private async Task<ApplicationUser?> GetUserFromSharedPlaylistId(string? sharedId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sharedId)) return null;
|
||||
|
||||
var playlist = await _sharedService.GetEntityByTokenAsync(sharedId);
|
||||
|
||||
if (playlist == null) return null;
|
||||
|
||||
if (!await _sharedService.CanPlayEveryoneAsync(playlist)) return null;
|
||||
|
||||
return await _userManager.FindByIdAsync(playlist.CreatorUserId.ToString());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class OpenIdController : ControllerBase
|
||||
var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
|
||||
if (user == null)
|
||||
{
|
||||
user = await _userManager.FindByEmailAsync(email);
|
||||
user = await _userManager.FindByEmailAsync(email!);
|
||||
if (user == null)
|
||||
{
|
||||
user = new ApplicationUser
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlaylistShared.Api.Entities;
|
||||
using PlaylistShared.Api.Extensions;
|
||||
using PlaylistShared.Api.Services;
|
||||
using PlaylistShared.Shared;
|
||||
using PlaylistShared.Shared.Enums;
|
||||
using PlaylistShared.Shared.SharedPlaylist;
|
||||
using PlaylistShared.Shared.Yandex;
|
||||
|
||||
@@ -19,18 +18,15 @@ public class PlaylistsController : ControllerBase
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SharedPlaylistService _sharedService;
|
||||
private readonly YandexMusicService _yandexService;
|
||||
private readonly YandexApiService _yandexApiService;
|
||||
|
||||
public PlaylistsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
SharedPlaylistService sharedService,
|
||||
YandexMusicService yandexService,
|
||||
YandexApiService yandexApiService)
|
||||
YandexMusicService yandexService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_sharedService = sharedService;
|
||||
_yandexService = yandexService;
|
||||
_yandexApiService = yandexApiService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -40,29 +36,30 @@ public class PlaylistsController : ControllerBase
|
||||
var user = await _userManager.FindByIdAsync(userId.ToString());
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var decryptedToken = _yandexApiService.DecryptToken(user.YandexAccessToken);
|
||||
if (string.IsNullOrEmpty(decryptedToken))
|
||||
if (string.IsNullOrEmpty(user.YandexAccessToken))
|
||||
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 400, Message = "Токен Яндекс.Музыки не установлен или недействителен" }));
|
||||
|
||||
var authSuccess = await _yandexApiService.AuthAsync(decryptedToken);
|
||||
if (!authSuccess)
|
||||
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 400, Message = "Не удалось авторизоваться в Яндекс.Музыке. Проверьте токен." }));
|
||||
|
||||
var favorites = await _yandexApiService.Client.Api.Playlist.FavoritesAsync();
|
||||
var ownPlaylists = favorites.Where(p => p.Owner.Uid == _yandexApiService.Client.Account.Uid).ToList();
|
||||
|
||||
var sharedPlaylists = await _sharedService.GetAllByUserAsync(userId);
|
||||
|
||||
var result = ownPlaylists.Select(p => new YandexPlaylistShare
|
||||
List<YandexPlaylistShare> result;
|
||||
try
|
||||
{
|
||||
Kind = p.Kind,
|
||||
OwnerUid = p.Owner.Uid,
|
||||
Title = p.Title,
|
||||
CoverUrl = p.Cover?.GetUrl() ?? "",
|
||||
TrackCount = p.TrackCount,
|
||||
IsShared = sharedPlaylists.Any(s => s.YandexPlaylistKind == p.Kind && s.YandexPlaylistOwnerUid == p.Owner.Uid),
|
||||
ShareToken = sharedPlaylists.FirstOrDefault(s => s.YandexPlaylistKind == p.Kind && s.YandexPlaylistOwnerUid == p.Owner.Uid)?.ShareToken,
|
||||
}).ToList();
|
||||
var (ownPlaylists, _) = await _yandexService.GetOwnFavoritesAsync(user);
|
||||
var sharedPlaylists = await _sharedService.GetAllByUserAsync(userId);
|
||||
|
||||
result = (ownPlaylists ?? []).Select(p => new YandexPlaylistShare
|
||||
{
|
||||
Kind = p.Kind,
|
||||
OwnerUid = p.Owner.Uid,
|
||||
Title = p.Title,
|
||||
CoverUrl = p.Cover?.GetUrl() ?? "",
|
||||
TrackCount = p.TrackCount,
|
||||
IsShared = sharedPlaylists.Any(s => s.YandexPlaylistKind == p.Kind && s.YandexPlaylistOwnerUid == p.Owner.Uid),
|
||||
ShareToken = sharedPlaylists.FirstOrDefault(s => s.YandexPlaylistKind == p.Kind && s.YandexPlaylistOwnerUid == p.Owner.Uid)?.ShareToken,
|
||||
}).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 400, Message = ex.Message }));
|
||||
}
|
||||
|
||||
return Ok(ApiResponse<List<YandexPlaylistShare>>.Ok(result));
|
||||
}
|
||||
@@ -74,7 +71,6 @@ public class PlaylistsController : ControllerBase
|
||||
var user = await _userManager.FindByIdAsync(userId.ToString());
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
// Проверяем, что плейлист действительно принадлежит пользователю
|
||||
var playlist = await _yandexService.GetPlaylistAsync(user, request.OwnerUid, request.Kind);
|
||||
if (playlist == null)
|
||||
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" }));
|
||||
@@ -86,13 +82,13 @@ public class PlaylistsController : ControllerBase
|
||||
YandexPlaylistOwnerUid = request.OwnerUid,
|
||||
Title = playlist.Title,
|
||||
Description = playlist.Description,
|
||||
ViewPermission = ViewPermission.Everyone,
|
||||
PlayPermission = ViewPermission.Everyone,
|
||||
AddPermission = EditPermission.AuthorizedOnly,
|
||||
RemovePermission = EditPermission.AddedByUserOnly,
|
||||
ViewPermission = Shared.Enums.ViewPermission.Everyone,
|
||||
PlayPermission = Shared.Enums.ViewPermission.Everyone,
|
||||
AddPermission = Shared.Enums.EditPermission.AuthorizedOnly,
|
||||
RemovePermission = Shared.Enums.EditPermission.AddedByUserOnly,
|
||||
};
|
||||
|
||||
var result = await _sharedService.CreateAsync(userId, dto);
|
||||
return Ok(ApiResponse<SharedPlaylistDto>.Ok(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ using PlaylistShared.Api.Services;
|
||||
using PlaylistShared.Shared;
|
||||
using PlaylistShared.Shared.SharedPlaylist;
|
||||
using PlaylistShared.Shared.Yandex;
|
||||
using YandexMusic.API.Models.Playlist;
|
||||
|
||||
namespace PlaylistShared.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
@@ -40,19 +41,16 @@ public class SharedPlaylistController : ControllerBase
|
||||
[HttpGet("{token}")]
|
||||
public async Task<ActionResult<ApiResponse<SharedPlaylistDto>>> GetByToken(string token)
|
||||
{
|
||||
var playlist = await _sharedService.GetByTokenAsync(token);
|
||||
if (playlist == null)
|
||||
var currentUserId = User.GetUserIdOrNull();
|
||||
|
||||
var entity = await _sharedService.GetEntityByTokenAsync(token);
|
||||
if (entity == null)
|
||||
return NotFound(ApiResponse<SharedPlaylistDto>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" }));
|
||||
|
||||
var currentUserId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||
var userIdGuid = !string.IsNullOrEmpty(currentUserId) ? Guid.Parse(currentUserId) : (Guid?)null;
|
||||
|
||||
// Проверка прав просмотра (требует доступа к сущности)
|
||||
var entity = await _sharedService.GetEntityByTokenAsync(token);
|
||||
if (entity == null || !await _sharedService.CanViewAsync(entity, userIdGuid))
|
||||
if (!await _sharedService.CanViewAsync(entity, currentUserId))
|
||||
return Unauthorized(ApiResponse<SharedPlaylistDto>.Fail(new ErrorResponse { StatusCode = 401, Message = "Недостаточно прав" }));
|
||||
|
||||
return Ok(ApiResponse<SharedPlaylistDto>.Ok(playlist));
|
||||
return Ok(ApiResponse<SharedPlaylistDto>.Ok(_sharedService.MapToDto(entity)));
|
||||
}
|
||||
|
||||
// GET /api/sharedplaylist/{token}/tracks
|
||||
@@ -71,11 +69,10 @@ public class SharedPlaylistController : ControllerBase
|
||||
if (creator == null)
|
||||
return StatusCode(500, ApiResponse<YandexPlaylistData>.Fail(new ErrorResponse { StatusCode = 500, Message = "Владелец плейлиста не найден" }));
|
||||
|
||||
var yandexPlaylist = await _yandexService.GetPlaylistAsync(creator, playlist.YandexPlaylistOwnerUid, playlist.YandexPlaylistKind);
|
||||
if (yandexPlaylist == null)
|
||||
var dto = await _yandexService.GetPlaylistDataAsync(creator, playlist.YandexPlaylistOwnerUid, playlist.YandexPlaylistKind);
|
||||
if (dto == null)
|
||||
return NotFound(ApiResponse<YandexPlaylistData>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден в Яндекс.Музыке" }));
|
||||
|
||||
var dto = MapToYandexPlaylistData(yandexPlaylist);
|
||||
return Ok(ApiResponse<YandexPlaylistData>.Ok(dto));
|
||||
}
|
||||
|
||||
@@ -129,6 +126,22 @@ public class SharedPlaylistController : ControllerBase
|
||||
return Ok(ApiResponse<object>.Ok(new { message = "Треки добавлены" }));
|
||||
}
|
||||
|
||||
// GET /api/sharedplaylist/{token}/additions
|
||||
[HttpGet("{token}/additions")]
|
||||
public async Task<ActionResult<ApiResponse<Dictionary<string, string?>>>> GetAdditions(string token)
|
||||
{
|
||||
var currentUserId = User.GetUserIdOrNull();
|
||||
var playlist = await _sharedService.GetEntityByTokenAsync(token);
|
||||
if (playlist == null)
|
||||
return NotFound(ApiResponse<Dictionary<string, string?>>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" }));
|
||||
|
||||
if (!await _sharedService.CanViewAsync(playlist, currentUserId))
|
||||
return Unauthorized();
|
||||
|
||||
var additions = await _trackAdditionLogService.GetAdditionUserNamesAsync(playlist.Id);
|
||||
return Ok(ApiResponse<Dictionary<string, string?>>.Ok(additions));
|
||||
}
|
||||
|
||||
// POST /api/sharedplaylist/{token}/remove-tracks
|
||||
[HttpPost("{token}/remove-tracks")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> RemoveTracks(string token, [FromBody] UpdateTrackListRequest request)
|
||||
@@ -164,26 +177,4 @@ public class SharedPlaylistController : ControllerBase
|
||||
return Ok(ApiResponse<object>.Ok(new { message = "Треки удалены" }));
|
||||
}
|
||||
|
||||
private YandexPlaylistData MapToYandexPlaylistData(YPlaylist playlist)
|
||||
{
|
||||
return new YandexPlaylistData
|
||||
{
|
||||
Title = playlist.Title,
|
||||
Description = playlist.Description,
|
||||
Tracks = playlist.Tracks.Select(t => new YandexTrack
|
||||
{
|
||||
TrackId = t.Track.Id,
|
||||
Title = t.Track.Title,
|
||||
Artists = t.Track.Artists.Select(t => new YandexArtist()
|
||||
{
|
||||
Id = t.Id,
|
||||
Name = t.Name,
|
||||
CoverUrl = t.Cover.GetUrl(),
|
||||
Description = t.Description?.Text ?? string.Empty,
|
||||
}).ToList(),
|
||||
DurationMs = (int)(t.Track?.DurationMs ?? 0),
|
||||
CoverUri = t.Track?.CoverUri ?? ""
|
||||
}).ToList() ?? new List<YandexTrack>()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlaylistShared.Api.Entities;
|
||||
@@ -31,11 +31,7 @@ public class YandexAccountController : ControllerBase
|
||||
var user = await _userManager.FindByIdAsync(userId.ToString());
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
user.YandexAccessToken = _yandexService.Service.EncryptToken(request.Token);
|
||||
// Не храним refresh-токен, так как пользователь вводит только access-токен
|
||||
user.YandexTokenExpiryUtc = DateTime.UtcNow.AddMonths(1); // условно, т.к. срок жизни токена неизвестен
|
||||
await _userManager.UpdateAsync(user);
|
||||
|
||||
await SaveYandexTokenAsync(user, request.Token);
|
||||
return Ok(ApiResponse<object>.Ok(new { message = "Токен сохранён" }));
|
||||
}
|
||||
|
||||
@@ -65,7 +61,6 @@ public class YandexAccountController : ControllerBase
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var qr = await _yandexService.GetQrOrGenerate(user);
|
||||
|
||||
return Ok(ApiResponse<YandexAuthQr>.Ok(qr));
|
||||
}
|
||||
|
||||
@@ -81,10 +76,16 @@ public class YandexAccountController : ControllerBase
|
||||
|
||||
if (checkResult.Status == Shared.Enums.YandexAuthQrStatus.Authorized)
|
||||
{
|
||||
await SetToken(new() { Token = _yandexService.Service.Client.AuthStorage.Token });
|
||||
|
||||
await SaveYandexTokenAsync(user, _yandexService.Service.Client.AuthStorage.Token);
|
||||
}
|
||||
|
||||
return Ok(ApiResponse<YandexAuthQrCheck>.Ok(checkResult));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveYandexTokenAsync(ApplicationUser user, string token)
|
||||
{
|
||||
user.YandexAccessToken = _yandexService.Service.EncryptToken(token);
|
||||
user.YandexTokenExpiryUtc = DateTime.UtcNow.AddMonths(1);
|
||||
await _userManager.UpdateAsync(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,12 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
builder.Entity<ApplicationUser>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => e.RefreshToken)
|
||||
.HasDatabaseName("IX_AspNetUsers_RefreshToken");
|
||||
});
|
||||
|
||||
builder.Entity<FavoritePlaylist>(entity =>
|
||||
{
|
||||
entity.HasKey(e => new { e.UserId, e.SharedPlaylistId });
|
||||
@@ -128,10 +134,12 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
|
||||
entity.Property(e => e.TrackId)
|
||||
.HasMaxLength(100)
|
||||
.IsRequired(false);
|
||||
entity.Property(e => e.CsfrToken)
|
||||
entity.Property(e => e.CsrfToken)
|
||||
.HasColumnName("CsfrToken")
|
||||
.HasMaxLength(200)
|
||||
.IsRequired(false);
|
||||
entity.Property(e => e.HeaderCsfrToken)
|
||||
entity.Property(e => e.HeaderCsrfToken)
|
||||
.HasColumnName("HeaderCsfrToken")
|
||||
.HasMaxLength(200)
|
||||
.IsRequired(false);
|
||||
entity.Property(e => e.HeaderProcessId)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
namespace PlaylistShared.Api.Entities;
|
||||
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 string QrCodeUrl { get; set; } = string.Empty;
|
||||
public string SerializedCookies { get; set; } = string.Empty;
|
||||
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 string? CsrfToken { get; set; }
|
||||
public string? HeaderProcessId { get; set; }
|
||||
public string? HeaderCsfrToken { get; set; }
|
||||
public string? HeaderCsrfToken { get; set; }
|
||||
|
||||
public ApplicationUser? User { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ public class Program
|
||||
{
|
||||
options.AddPolicy("Production", policy =>
|
||||
{
|
||||
policy.WithOrigins(builder.Configuration.GetSection("Cors:Origins").Get<string[]>())
|
||||
policy.WithOrigins(builder.Configuration.GetSection("Cors:Origins").Get<string[]>() ?? [])
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials();
|
||||
@@ -123,18 +123,12 @@ public class Program
|
||||
{
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
});
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapOpenApi();
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
|
||||
app.UseCors("Production");
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
|
||||
@@ -90,10 +90,10 @@ public class FavoritesService
|
||||
Creator = sp.Creator != null ? new Shared.Auth.ApplicationUserDto
|
||||
{
|
||||
Id = sp.Creator.Id,
|
||||
UserName = sp.Creator.UserName,
|
||||
Email = sp.Creator.Email,
|
||||
UserName = sp.Creator.UserName ?? "",
|
||||
Email = sp.Creator.Email ?? "",
|
||||
YandexId = sp.Creator.YandexId,
|
||||
DisplayName = sp.Creator.UserName
|
||||
DisplayName = sp.Creator.UserName ?? ""
|
||||
} : null
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using PlaylistShared.Api.Entities;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
@@ -11,11 +12,15 @@ public class JwtService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ITimeLimitedDataProtector _playTokenProtector;
|
||||
|
||||
public JwtService(IConfiguration configuration, UserManager<ApplicationUser> userManager)
|
||||
public JwtService(IConfiguration configuration, UserManager<ApplicationUser> userManager, IDataProtectionProvider dataProtectionProvider)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_userManager = userManager;
|
||||
_playTokenProtector = dataProtectionProvider
|
||||
.CreateProtector("AudioPlayToken")
|
||||
.ToTimeLimitedDataProtector();
|
||||
}
|
||||
|
||||
public async Task<(string Token, string RefreshToken, DateTime Expiration)> GenerateTokenAsync(ApplicationUser user)
|
||||
@@ -71,4 +76,20 @@ public class JwtService
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string CreatePlayToken(Guid userId) =>
|
||||
_playTokenProtector.Protect(userId.ToString(), TimeSpan.FromMinutes(5));
|
||||
|
||||
public Guid? ValidatePlayToken(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = _playTokenProtector.Unprotect(token);
|
||||
return Guid.Parse(userId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,14 +42,6 @@ public class SharedPlaylistService
|
||||
return MapToDto(entity);
|
||||
}
|
||||
|
||||
public async Task<SharedPlaylistDto?> GetByTokenAsync(string token)
|
||||
{
|
||||
var entity = await _db.SharedPlaylists
|
||||
.Include(sp => sp.Creator)
|
||||
.FirstOrDefaultAsync(sp => sp.ShareToken == token && !sp.IsDeleted);
|
||||
return entity == null ? null : MapToDto(entity);
|
||||
}
|
||||
|
||||
public async Task<SharedPlaylist?> GetEntityByTokenAsync(string token)
|
||||
{
|
||||
return await _db.SharedPlaylists
|
||||
@@ -142,8 +134,7 @@ public class SharedPlaylistService
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
// Ручное маппинг сущности в DTO
|
||||
private SharedPlaylistDto MapToDto(SharedPlaylist entity)
|
||||
public SharedPlaylistDto MapToDto(SharedPlaylist entity)
|
||||
{
|
||||
return new SharedPlaylistDto
|
||||
{
|
||||
|
||||
@@ -43,4 +43,17 @@ public class TrackAdditionLogService
|
||||
_db.TrackAdditionLogs.RemoveRange(logs);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, string?>> GetAdditionUserNamesAsync(Guid sharedPlaylistId)
|
||||
{
|
||||
var rows = await _db.TrackAdditionLogs
|
||||
.Where(l => l.SharedPlaylistId == sharedPlaylistId)
|
||||
.Include(l => l.AddedByUser)
|
||||
.OrderByDescending(l => l.AddedAtUtc)
|
||||
.ToListAsync();
|
||||
|
||||
return rows
|
||||
.GroupBy(l => l.TrackId)
|
||||
.ToDictionary(g => g.Key, g => g.First().AddedByUser?.UserName);
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public class YandexApiService : IDisposable
|
||||
/// </summary>
|
||||
/// <param name="encryptedToken"></param>
|
||||
/// <returns></returns>
|
||||
public string DecryptToken(string encryptedToken)
|
||||
public string? DecryptToken(string encryptedToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PlaylistShared.Api.Data;
|
||||
using PlaylistShared.Api.Entities;
|
||||
using PlaylistShared.Shared.Yandex;
|
||||
@@ -24,10 +24,10 @@ public class YandexAuthService
|
||||
|
||||
internal async Task<YandexAuthQr> GetQrOrGenerate(ApplicationUser user)
|
||||
{
|
||||
var existingSession = _dbContext.YandexAuthSessions
|
||||
var existingSession = await _dbContext.YandexAuthSessions
|
||||
.Where(s => s.UserId == user.Id && !s.IsConfirmed && s.CreatedAt > DateTime.UtcNow.AddMinutes(-5))
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.FirstOrDefault();
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (existingSession != null)
|
||||
{
|
||||
@@ -45,14 +45,14 @@ public class YandexAuthService
|
||||
{
|
||||
var qr = await Api.Passport.GetAuthQRLinkAsync();
|
||||
var trackId = Service.Client.AuthStorage.AuthToken.TrackId;
|
||||
var csfrToken = Service.Client.AuthStorage.AuthToken.CsfrToken;
|
||||
var csrfToken = Service.Client.AuthStorage.AuthToken.CsfrToken;
|
||||
var headerProcessUuid = Service.Client.AuthStorage.HeaderToken.ProcessUuid;
|
||||
var headerCsfrToken = Service.Client.AuthStorage.HeaderToken.CsfrToken;
|
||||
var headerCsrfToken = Service.Client.AuthStorage.HeaderToken.CsfrToken;
|
||||
|
||||
if (string.IsNullOrEmpty(qr))
|
||||
throw new Exception("Не удалось получить QR-ссылку");
|
||||
|
||||
var cookiesJson = SerializeCookies(_apiService.CookieContainer);
|
||||
var cookiesJson = _apiService.EncryptToken(SerializeCookies(_apiService.CookieContainer));
|
||||
|
||||
var session = new YandexAuthSession
|
||||
{
|
||||
@@ -62,10 +62,9 @@ public class YandexAuthService
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
IsConfirmed = false,
|
||||
TrackId = trackId,
|
||||
CsfrToken = csfrToken,
|
||||
HeaderCsfrToken = headerCsfrToken,
|
||||
CsrfToken = csrfToken,
|
||||
HeaderCsrfToken = headerCsrfToken,
|
||||
HeaderProcessId = headerProcessUuid,
|
||||
|
||||
};
|
||||
|
||||
_dbContext.YandexAuthSessions.Add(session);
|
||||
@@ -83,16 +82,19 @@ public class YandexAuthService
|
||||
var session = await _dbContext.YandexAuthSessions.FindAsync(sessionId);
|
||||
if (session == null) return null;
|
||||
|
||||
RestoreCookies(Service.CookieContainer, session.SerializedCookies);
|
||||
var decryptedCookies = _apiService.DecryptToken(session.SerializedCookies);
|
||||
if (decryptedCookies == null) return null;
|
||||
|
||||
RestoreCookies(Service.CookieContainer, decryptedCookies);
|
||||
if (Service.Client.AuthStorage.AuthToken is null)
|
||||
{
|
||||
Service.Client.AuthStorage.AuthToken = new();
|
||||
}
|
||||
|
||||
Service.Client.AuthStorage.AuthToken.CsfrToken = session?.CsfrToken ?? "";
|
||||
Service.Client.AuthStorage.AuthToken.TrackId = session?.TrackId ?? "";
|
||||
Service.Client.AuthStorage.HeaderToken.CsfrToken = session?.HeaderCsfrToken ?? "";
|
||||
Service.Client.AuthStorage.HeaderToken.ProcessUuid = session?.HeaderProcessId ?? "";
|
||||
Service.Client.AuthStorage.AuthToken.CsfrToken = session.CsrfToken ?? "";
|
||||
Service.Client.AuthStorage.AuthToken.TrackId = session.TrackId ?? "";
|
||||
Service.Client.AuthStorage.HeaderToken.CsfrToken = session.HeaderCsrfToken ?? "";
|
||||
Service.Client.AuthStorage.HeaderToken.ProcessUuid = session.HeaderProcessId ?? "";
|
||||
|
||||
var status = await Api.Passport.CheckQRStatusAsync();
|
||||
|
||||
@@ -100,36 +102,29 @@ public class YandexAuthService
|
||||
{
|
||||
try
|
||||
{
|
||||
var auth = await Api.Passport.AuthorizeByQRAsync();
|
||||
await Api.Passport.AuthorizeByQRAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
return new() { Status = Shared.Enums.YandexAuthQrStatus.Error, };
|
||||
return new() { Status = Shared.Enums.YandexAuthQrStatus.Error };
|
||||
}
|
||||
|
||||
_dbContext.YandexAuthSessions.Where(t => t.UserId == session.UserId).ExecuteDelete();
|
||||
_dbContext.SaveChanges();
|
||||
await _dbContext.YandexAuthSessions
|
||||
.Where(t => t.UserId == session.UserId)
|
||||
.ExecuteDeleteAsync();
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
return new() { Status = Shared.Enums.YandexAuthQrStatus.Authorized, };
|
||||
return new() { Status = Shared.Enums.YandexAuthQrStatus.Authorized };
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
Status = Shared.Enums.YandexAuthQrStatus.Pending,
|
||||
};
|
||||
return new() { Status = Shared.Enums.YandexAuthQrStatus.Pending };
|
||||
}
|
||||
|
||||
|
||||
private string SerializeCookies(CookieContainer container)
|
||||
{
|
||||
var allCookies = new List<object>();
|
||||
|
||||
var cookies = container.GetAllCookies();
|
||||
foreach (Cookie cookie in cookies)
|
||||
{
|
||||
foreach (Cookie cookie in container.GetAllCookies())
|
||||
allCookies.Add(new { cookie.Name, cookie.Value, cookie.Domain, cookie.Path });
|
||||
}
|
||||
|
||||
return JsonSerializer.Serialize(allCookies);
|
||||
}
|
||||
|
||||
@@ -137,9 +132,7 @@ public class YandexAuthService
|
||||
{
|
||||
var cookies = JsonSerializer.Deserialize<List<CookieData>>(serializedCookies);
|
||||
foreach (var c in cookies)
|
||||
{
|
||||
container.Add(new Cookie(c.Name, c.Value, c.Path, c.Domain));
|
||||
}
|
||||
}
|
||||
|
||||
private class CookieData
|
||||
@@ -149,4 +142,4 @@ public class YandexAuthService
|
||||
public string Domain { get; set; }
|
||||
public string Path { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,21 @@ public class YandexMusicService
|
||||
return await Api.Playlist.GetAsync(ownerUid, kind);
|
||||
}
|
||||
|
||||
public async Task<YandexPlaylistData?> GetPlaylistDataAsync(ApplicationUser user, string ownerUid, string kind)
|
||||
{
|
||||
var playlist = await GetPlaylistAsync(user, ownerUid, kind);
|
||||
return playlist == null ? null : MapToPlaylistData(playlist);
|
||||
}
|
||||
|
||||
public async Task<(List<YPlaylist>? OwnPlaylists, string? AccountUid)> GetOwnFavoritesAsync(ApplicationUser user)
|
||||
{
|
||||
await AuthorizeIfNot(user);
|
||||
var favorites = await Api.Playlist.FavoritesAsync();
|
||||
var accountUid = _yandexApiService.Client.Account?.Uid;
|
||||
var ownPlaylists = favorites?.Where(p => p.Owner?.Uid == accountUid).ToList();
|
||||
return (ownPlaylists, accountUid);
|
||||
}
|
||||
|
||||
public async Task<YPlaylist?> CreatePlaylistAsync(ApplicationUser user, string title)
|
||||
{
|
||||
await AuthorizeIfNot(user);
|
||||
@@ -339,4 +354,24 @@ public class YandexMusicService
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static YandexPlaylistData MapToPlaylistData(YPlaylist playlist) => new()
|
||||
{
|
||||
Title = playlist.Title,
|
||||
Description = playlist.Description,
|
||||
Tracks = playlist.Tracks.Select(t => new YandexTrack
|
||||
{
|
||||
TrackId = t.Track.Id,
|
||||
Title = t.Track.Title,
|
||||
Artists = t.Track.Artists.Select(a => new YandexArtist
|
||||
{
|
||||
Id = a.Id,
|
||||
Name = a.Name,
|
||||
CoverUrl = a.Cover.GetUrl(),
|
||||
Description = a.Description?.Text ?? string.Empty,
|
||||
}).ToList(),
|
||||
DurationMs = (int)(t.Track?.DurationMs ?? 0),
|
||||
CoverUri = t.Track?.CoverUri ?? ""
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user