Добавьте файлы проекта.

This commit is contained in:
FrigaT
2026-04-13 14:16:44 +03:00
parent b2b5a3945a
commit 37c997dbe0
120 changed files with 5364 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using PlaylistShared.Api.Entities;
using PlaylistShared.Api.Services;
using PlaylistShared.Shared.DTO;
[ApiController]
[Route("api/[controller]")]
public class AccountController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly JwtService _jwtService;
public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager, JwtService jwtService)
{
_userManager = userManager;
_signInManager = signInManager;
_jwtService = jwtService;
}
[HttpPost("register")]
public async Task<ActionResult<ApiResponse<LoginResponse>>> Register(RegisterRequest request)
{
var user = new ApplicationUser
{
UserName = request.Username,
Email = request.Email
};
var result = await _userManager.CreateAsync(user, request.Password);
if (!result.Succeeded)
return BadRequest(ApiResponse<LoginResponse>.Fail(new ErrorResponse
{
StatusCode = 400,
Message = string.Join(", ", result.Errors.Select(e => e.Description))
}));
return await GenerateTokenResponse(user);
}
[HttpPost("login")]
public async Task<ActionResult<ApiResponse<LoginResponse>>> Login(LoginRequest request)
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
return Unauthorized(ApiResponse<LoginResponse>.Fail(new ErrorResponse { StatusCode = 401, Message = "Неверное имя пользователя или пароль" }));
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
if (!result.Succeeded)
return Unauthorized(ApiResponse<LoginResponse>.Fail(new ErrorResponse { StatusCode = 401, Message = "Неверное имя пользователя или пароль" }));
return await GenerateTokenResponse(user);
}
private async Task<ActionResult<ApiResponse<LoginResponse>>> GenerateTokenResponse(ApplicationUser user)
{
var (token, refreshToken, expiration) = await _jwtService.GenerateTokenAsync(user);
return Ok(ApiResponse<LoginResponse>.Ok(new LoginResponse
{
Token = token,
RefreshToken = refreshToken,
Expiration = expiration
}));
}
[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);
if (user == null)
return Unauthorized(ApiResponse<LoginResponse>.Fail(new ErrorResponse { StatusCode = 401, Message = "Неверный или просроченный refresh token" }));
return await GenerateTokenResponse(user);
}
}

View File

@@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using PlaylistShared.Api.Entities;
using PlaylistShared.Api.Services;
using System.Security.Claims;
namespace PlaylistShared.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class OpenIdController : ControllerBase
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly JwtService _jwtService;
private readonly IConfiguration _configuration;
public OpenIdController(
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
JwtService jwtService,
IConfiguration configuration)
{
_signInManager = signInManager;
_userManager = userManager;
_jwtService = jwtService;
_configuration = configuration;
}
[HttpGet("login")]
public IActionResult Login(string? returnUrl = null)
{
var redirectUrl = Url.Action(nameof(Callback), "OpenId", new { returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties("Keycloak", redirectUrl);
return Challenge(properties, "Keycloak");
}
[HttpGet("callback")]
public async Task<IActionResult> Callback(string? returnUrl = null, string? remoteError = null)
{
if (remoteError != null)
return BadRequest($"Ошибка внешнего входа: {remoteError}");
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
return BadRequest("Не удалось получить информацию от провайдера");
var email = info.Principal.FindFirst(ClaimTypes.Email)?.Value;
var userName = info.Principal.FindFirst(ClaimTypes.Name)?.Value ?? email;
var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
if (user == null)
{
user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
user = new ApplicationUser
{
UserName = userName,
Email = email
};
var createResult = await _userManager.CreateAsync(user);
if (!createResult.Succeeded)
return BadRequest(createResult.Errors);
}
var loginResult = await _userManager.AddLoginAsync(user, info);
if (!loginResult.Succeeded)
return BadRequest(loginResult.Errors);
}
await _signInManager.SignInAsync(user, isPersistent: false);
var (token, refreshToken, _) = await _jwtService.GenerateTokenAsync(user);
return Redirect($"{_configuration["Client:BaseUrl"]}/auth-callback?token={token}&refreshToken={refreshToken}");
}
}

View File

@@ -0,0 +1,174 @@
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.DTO;
using PlaylistShared.Shared.Enums;
using PlaylistShared.Shared.Models;
using YandexMusic;
namespace PlaylistShared.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class PlaylistController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SharedPlaylistService _sharedService;
private readonly YandexMusicService _yandexService;
private readonly TrackAdditionLogService _trackLogService;
public PlaylistController(
UserManager<ApplicationUser> userManager,
SharedPlaylistService sharedService,
YandexMusicService yandexService,
TrackAdditionLogService trackLogService)
{
_userManager = userManager;
_sharedService = sharedService;
_yandexService = yandexService;
_trackLogService = trackLogService;
}
[HttpPost("add-tracks")]
public async Task<ActionResult<ApiResponse<object>>> AddTracks([FromBody] AddTrackRequest request)
{
var currentUserId = User.GetUserId();
var playlist = await _sharedService.GetEntityByTokenAsync(request.SharedPlaylistToken);
if (playlist == null)
return NotFound(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" }));
if (!await _sharedService.CanAddTrackAsync(playlist, currentUserId))
return StatusCode(403, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 403, Message = "Недостаточно прав для добавления треков" }));
var creator = await _userManager.FindByIdAsync(playlist.CreatorUserId.ToString());
if (creator == null)
return StatusCode(500, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 500, Message = "Владелец плейлиста не найден" }));
var updatedPlaylist = await _yandexService.AddTracksAsync(creator, playlist.YandexPlaylistOwnerUid, playlist.YandexPlaylistKind, request.TrackIds);
if (updatedPlaylist == null)
return StatusCode(500, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 500, Message = "Ошибка при добавлении треков в Яндекс.Музыку" }));
// Логируем добавления для права AddedByUserOnly
foreach (var trackId in request.TrackIds)
await _trackLogService.LogAdditionAsync(playlist.Id, trackId, currentUserId);
return Ok(ApiResponse<object>.Ok(new { message = "Треки успешно добавлены" }));
}
[HttpPost("remove-tracks")]
public async Task<ActionResult<ApiResponse<object>>> RemoveTracks([FromBody] AddTrackRequest request)
{
var currentUserId = User.GetUserId();
var playlist = await _sharedService.GetEntityByTokenAsync(request.SharedPlaylistToken);
if (playlist == null)
return NotFound(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" }));
// Проверяем права на удаление каждого трека
foreach (var trackId in request.TrackIds)
{
if (!await _sharedService.CanRemoveTrackAsync(playlist, currentUserId, trackId))
return StatusCode(403, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 403, Message = $"Недостаточно прав для удаления трека {trackId}" }));
}
var creator = await _userManager.FindByIdAsync(playlist.CreatorUserId.ToString());
if (creator == null)
return StatusCode(500, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 500, Message = "Владелец плейлиста не найден" }));
var updatedPlaylist = await _yandexService.RemoveTracksAsync(creator, playlist.YandexPlaylistOwnerUid, playlist.YandexPlaylistKind, request.TrackIds);
if (updatedPlaylist == null)
return StatusCode(500, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 500, Message = "Ошибка при удалении треков из Яндекс.Музыки" }));
// Удаляем логи добавления для этих треков
foreach (var trackId in request.TrackIds)
await _trackLogService.RemoveLogsForTrackAsync(playlist.Id, trackId);
return Ok(ApiResponse<object>.Ok(new { message = "Треки успешно удалены" }));
}
[HttpGet("info/{ownerUid}/{kind}")]
public async Task<ActionResult<ApiResponse<object>>> GetPlaylistInfo(string ownerUid, string kind)
{
var currentUserId = User.GetUserId();
// Найти шеринг-плейлист по данным Яндекс
var shared = await _sharedService.GetEntityByTokenAsync(null); // не можем по токену, надо по параметрам
// Для простоты сделаем отдельный метод поиска по kind/ownerUid
var playlistEntity = await _sharedService.GetByYandexIdsAsync(ownerUid, kind);
if (playlistEntity == null)
return NotFound(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" }));
if (!await _sharedService.CanViewAsync(playlistEntity, currentUserId))
return Unauthorized();
var creator = await _userManager.FindByIdAsync(playlistEntity.CreatorUserId.ToString());
var yandexPlaylist = await _yandexService.GetPlaylistAsync(creator, ownerUid, kind);
return Ok(ApiResponse<object>.Ok(yandexPlaylist));
}
[HttpGet("my")]
public async Task<ActionResult<ApiResponse<List<YandexPlaylistInfo>>>> GetMyPlaylists()
{
var userId = User.GetUserId();
var user = await _userManager.FindByIdAsync(userId.ToString());
if (user == null) return Unauthorized();
var decryptedToken = _yandexService.DecryptToken(user.YandexAccessToken);
if (string.IsNullOrEmpty(decryptedToken))
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 400, Message = "Токен Яндекс.Музыки не установлен или недействителен" }));
var yandexClient = new YandexMusicClient();
var authSuccess = await yandexClient.Authorize(decryptedToken);
if (!authSuccess)
return BadRequest(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 400, Message = "Не удалось авторизоваться в Яндекс.Музыке. Проверьте токен." }));
var favorites = await yandexClient.GetFavoritesAsync();
var ownPlaylists = favorites.Where(p => p.Owner.Uid == yandexClient.Account.Uid).ToList();
var sharedPlaylists = await _sharedService.GetAllByUserAsync(userId);
var result = ownPlaylists.Select(p => new YandexPlaylistInfo
{
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();
return Ok(ApiResponse<List<YandexPlaylistInfo>>.Ok(result));
}
[HttpPost("share")]
public async Task<ActionResult<ApiResponse<SharedPlaylistDto>>> SharePlaylist([FromBody] SharePlaylistRequest request)
{
var userId = User.GetUserId();
var user = await _userManager.FindByIdAsync(userId.ToString());
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<object>.Fail(new ErrorResponse { StatusCode = 400, Message = "Плейлист не принадлежит вам" }));
var dto = new SharePlaylistDto
{
YandexPlaylistKind = request.Kind,
YandexPlaylistOwnerUid = request.OwnerUid,
Title = playlist.Title,
Description = playlist.Description,
ViewPermission = ViewPermission.Everyone,
AddPermission = EditPermission.AuthorizedOnly,
RemovePermission = EditPermission.AddedByUserOnly
};
var result = await _sharedService.CreateAsync(userId, dto);
return Ok(ApiResponse<SharedPlaylistDto>.Ok(result));
}
}

View File

@@ -0,0 +1,71 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PlaylistShared.Api.Extensions;
using PlaylistShared.Api.Services;
using PlaylistShared.Shared.DTO;
using PlaylistShared.Shared.Models;
namespace PlaylistShared.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class SharedPlaylistController : ControllerBase
{
private readonly SharedPlaylistService _sharedService;
private readonly YandexMusicService _yandexService;
public SharedPlaylistController(SharedPlaylistService sharedService, YandexMusicService yandexService)
{
_sharedService = sharedService;
_yandexService = yandexService;
}
[HttpPost]
[Authorize]
public async Task<ActionResult<ApiResponse<SharedPlaylistDto>>> Create([FromBody] SharePlaylistDto dto)
{
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId) || !Guid.TryParse(userId, out var guid))
return Unauthorized();
var result = await _sharedService.CreateAsync(guid, dto);
return Ok(ApiResponse<SharedPlaylistDto>.Ok(result));
}
[HttpGet("{token}")]
public async Task<ActionResult<ApiResponse<SharedPlaylistDto>>> GetByToken(string token)
{
var playlist = await _sharedService.GetByTokenAsync(token);
if (playlist == 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))
return Unauthorized(ApiResponse<SharedPlaylistDto>.Fail(new ErrorResponse { StatusCode = 401, Message = "Недостаточно прав" }));
return Ok(ApiResponse<SharedPlaylistDto>.Ok(playlist));
}
[HttpPut("{token}/permissions")]
[Authorize]
public async Task<ActionResult<ApiResponse<SharedPlaylistDto>>> UpdatePermissions(string token, [FromBody] UpdatePermissionsDto dto)
{
var userId = User.GetUserId();
var playlist = await _sharedService.GetEntityByTokenAsync(token);
if (playlist == null)
return NotFound(ApiResponse<SharedPlaylistDto>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" }));
if (playlist.CreatorUserId != userId)
return Forbid();
var updated = await _sharedService.UpdatePermissionsAsync(playlist.Id, dto);
if (updated == null)
return BadRequest(ApiResponse<SharedPlaylistDto>.Fail(new ErrorResponse { StatusCode = 400, Message = "Ошибка обновления прав" }));
return Ok(ApiResponse<SharedPlaylistDto>.Ok(updated));
}
}

View File

@@ -0,0 +1,57 @@
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.DTO;
namespace PlaylistShared.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class YandexTokenController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly YandexMusicService _yandexService;
public YandexTokenController(UserManager<ApplicationUser> userManager, YandexMusicService yandexService)
{
_userManager = userManager;
_yandexService = yandexService;
}
[HttpPost("set")]
public async Task<ActionResult<ApiResponse<object>>> 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);
// Не храним refresh-токен, так как пользователь вводит только access-токен
user.YandexTokenExpiryUtc = DateTime.UtcNow.AddMonths(1); // условно, т.к. срок жизни токена неизвестен
await _userManager.UpdateAsync(user);
return Ok(ApiResponse<object>.Ok(new { message = "Токен сохранён" }));
}
[HttpGet("status")]
public async Task<ActionResult<ApiResponse<YandexTokenStatus>>> GetStatus()
{
var userId = User.GetUserId();
var user = await _userManager.FindByIdAsync(userId.ToString());
if (user == null) return Unauthorized();
var hasToken = !string.IsNullOrEmpty(user.YandexAccessToken);
var isValid = hasToken && user.YandexTokenExpiryUtc > DateTime.UtcNow;
return Ok(ApiResponse<YandexTokenStatus>.Ok(new YandexTokenStatus
{
HasToken = hasToken,
IsValid = isValid,
ExpiryUtc = user.YandexTokenExpiryUtc
}));
}
}