Проведен аудит. Добавлено переключение треков

This commit is contained in:
FrigaT
2026-05-21 20:49:55 +03:00
parent 38af6174fa
commit 9139d8ecfe
23 changed files with 351 additions and 222 deletions

View File

@@ -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" }));

View File

@@ -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());
}
}
}

View File

@@ -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

View File

@@ -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));
}
}
}

View File

@@ -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>()
};
}
}

View File

@@ -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);
}
}