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

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

View File

@@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using PlaylistShared.Api.Entities;
namespace PlaylistShared.Api.Data;
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
public DbSet<SharedPlaylistEntity> SharedPlaylists => Set<SharedPlaylistEntity>();
public DbSet<TrackAdditionLogEntity> TrackAdditionLogs => Set<TrackAdditionLogEntity>();
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<SharedPlaylistEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.ShareToken).IsUnique();
entity.HasOne(e => e.Creator)
.WithMany(u => u.OwnedPlaylists)
.HasForeignKey(e => e.CreatorUserId)
.OnDelete(DeleteBehavior.Restrict);
entity.Property(e => e.YandexPlaylistKind).IsRequired().HasMaxLength(50);
entity.Property(e => e.YandexPlaylistOwnerUid).IsRequired().HasMaxLength(50);
entity.Property(e => e.Title).IsRequired().HasMaxLength(255);
});
builder.Entity<TrackAdditionLogEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => new { e.SharedPlaylistId, e.TrackId });
entity.HasOne(e => e.SharedPlaylist)
.WithMany(sp => sp.TrackAdditionLogs)
.HasForeignKey(e => e.SharedPlaylistId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.AddedByUser)
.WithMany()
.HasForeignKey(e => e.AddedByUserId)
.OnDelete(DeleteBehavior.Restrict);
});
}
}

View File

@@ -0,0 +1,426 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using PlaylistShared.Api.Data;
#nullable disable
namespace PlaylistShared.Api.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260412171234_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("RoleId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("RoleId")
.HasColumnType("uniqueidentifier");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("RefreshToken")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("RefreshTokenExpiryUtc")
.HasColumnType("datetime2");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("YandexAccessToken")
.HasColumnType("nvarchar(max)");
b.Property<string>("YandexId")
.HasColumnType("nvarchar(max)");
b.Property<string>("YandexRefreshToken")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("YandexTokenExpiryUtc")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylistEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("AddPermission")
.HasColumnType("int");
b.Property<string>("CoverUrl")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid>("CreatorUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<int>("RemovePermission")
.HasColumnType("int");
b.Property<string>("ShareToken")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<int>("ViewPermission")
.HasColumnType("int");
b.Property<string>("YandexPlaylistKind")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("YandexPlaylistOwnerUid")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.HasIndex("CreatorUserId");
b.HasIndex("ShareToken")
.IsUnique();
b.ToTable("SharedPlaylists");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLogEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("AddedAtUtc")
.HasColumnType("datetime2");
b.Property<Guid>("AddedByUserId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("SharedPlaylistId")
.HasColumnType("uniqueidentifier");
b.Property<string>("TrackId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("AddedByUserId");
b.HasIndex("SharedPlaylistId", "TrackId");
b.ToTable("TrackAdditionLogs");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylistEntity", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
.WithMany("OwnedPlaylists")
.HasForeignKey("CreatorUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Creator");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLogEntity", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser")
.WithMany()
.HasForeignKey("AddedByUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylistEntity", "SharedPlaylist")
.WithMany("TrackAdditionLogs")
.HasForeignKey("SharedPlaylistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AddedByUser");
b.Navigation("SharedPlaylist");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
{
b.Navigation("OwnedPlaylists");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylistEntity", b =>
{
b.Navigation("TrackAdditionLogs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,314 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PlaylistShared.Api.Data.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
YandexId = table.Column<string>(type: "nvarchar(max)", nullable: true),
YandexAccessToken = table.Column<string>(type: "nvarchar(max)", nullable: true),
YandexRefreshToken = table.Column<string>(type: "nvarchar(max)", nullable: true),
YandexTokenExpiryUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
RefreshToken = table.Column<string>(type: "nvarchar(max)", nullable: true),
RefreshTokenExpiryUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
UserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "bit", nullable: false),
PasswordHash = table.Column<string>(type: "nvarchar(max)", nullable: true),
SecurityStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
PhoneNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "bit", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "bit", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
LockoutEnabled = table.Column<bool>(type: "bit", nullable: false),
AccessFailedCount = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
RoleId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(type: "nvarchar(450)", nullable: false),
ProviderKey = table.Column<string>(type: "nvarchar(450)", nullable: false),
ProviderDisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
table.ForeignKey(
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
RoleId = table.Column<Guid>(type: "uniqueidentifier", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
LoginProvider = table.Column<string>(type: "nvarchar(450)", nullable: false),
Name = table.Column<string>(type: "nvarchar(450)", nullable: false),
Value = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
table.ForeignKey(
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "SharedPlaylists",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
CreatorUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
YandexPlaylistKind = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
YandexPlaylistOwnerUid = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Title = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
CoverUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
ShareToken = table.Column<string>(type: "nvarchar(450)", nullable: false),
ViewPermission = table.Column<int>(type: "int", nullable: false),
AddPermission = table.Column<int>(type: "int", nullable: false),
RemovePermission = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SharedPlaylists", x => x.Id);
table.ForeignKey(
name: "FK_SharedPlaylists_AspNetUsers_CreatorUserId",
column: x => x.CreatorUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "TrackAdditionLogs",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
SharedPlaylistId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TrackId = table.Column<string>(type: "nvarchar(450)", nullable: false),
AddedByUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
AddedAtUtc = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TrackAdditionLogs", x => x.Id);
table.ForeignKey(
name: "FK_TrackAdditionLogs_AspNetUsers_AddedByUserId",
column: x => x.AddedByUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_TrackAdditionLogs_SharedPlaylists_SharedPlaylistId",
column: x => x.SharedPlaylistId,
principalTable: "SharedPlaylists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "AspNetRoles",
column: "NormalizedName",
unique: true,
filter: "[NormalizedName] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserClaims_UserId",
table: "AspNetUserClaims",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserLogins_UserId",
table: "AspNetUserLogins",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserRoles_RoleId",
table: "AspNetUserRoles",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "EmailIndex",
table: "AspNetUsers",
column: "NormalizedEmail");
migrationBuilder.CreateIndex(
name: "UserNameIndex",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true,
filter: "[NormalizedUserName] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_SharedPlaylists_CreatorUserId",
table: "SharedPlaylists",
column: "CreatorUserId");
migrationBuilder.CreateIndex(
name: "IX_SharedPlaylists_ShareToken",
table: "SharedPlaylists",
column: "ShareToken",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_TrackAdditionLogs_AddedByUserId",
table: "TrackAdditionLogs",
column: "AddedByUserId");
migrationBuilder.CreateIndex(
name: "IX_TrackAdditionLogs_SharedPlaylistId_TrackId",
table: "TrackAdditionLogs",
columns: new[] { "SharedPlaylistId", "TrackId" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AspNetRoleClaims");
migrationBuilder.DropTable(
name: "AspNetUserClaims");
migrationBuilder.DropTable(
name: "AspNetUserLogins");
migrationBuilder.DropTable(
name: "AspNetUserRoles");
migrationBuilder.DropTable(
name: "AspNetUserTokens");
migrationBuilder.DropTable(
name: "TrackAdditionLogs");
migrationBuilder.DropTable(
name: "AspNetRoles");
migrationBuilder.DropTable(
name: "SharedPlaylists");
migrationBuilder.DropTable(
name: "AspNetUsers");
}
}
}

View File

@@ -0,0 +1,423 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using PlaylistShared.Api.Data;
#nullable disable
namespace PlaylistShared.Api.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("RoleId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("RoleId")
.HasColumnType("uniqueidentifier");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("RefreshToken")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("RefreshTokenExpiryUtc")
.HasColumnType("datetime2");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("YandexAccessToken")
.HasColumnType("nvarchar(max)");
b.Property<string>("YandexId")
.HasColumnType("nvarchar(max)");
b.Property<string>("YandexRefreshToken")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("YandexTokenExpiryUtc")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylistEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("AddPermission")
.HasColumnType("int");
b.Property<string>("CoverUrl")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid>("CreatorUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<int>("RemovePermission")
.HasColumnType("int");
b.Property<string>("ShareToken")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<int>("ViewPermission")
.HasColumnType("int");
b.Property<string>("YandexPlaylistKind")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("YandexPlaylistOwnerUid")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.HasIndex("CreatorUserId");
b.HasIndex("ShareToken")
.IsUnique();
b.ToTable("SharedPlaylists");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLogEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("AddedAtUtc")
.HasColumnType("datetime2");
b.Property<Guid>("AddedByUserId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("SharedPlaylistId")
.HasColumnType("uniqueidentifier");
b.Property<string>("TrackId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("AddedByUserId");
b.HasIndex("SharedPlaylistId", "TrackId");
b.ToTable("TrackAdditionLogs");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylistEntity", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
.WithMany("OwnedPlaylists")
.HasForeignKey("CreatorUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Creator");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLogEntity", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser")
.WithMany()
.HasForeignKey("AddedByUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylistEntity", "SharedPlaylist")
.WithMany("TrackAdditionLogs")
.HasForeignKey("SharedPlaylistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AddedByUser");
b.Navigation("SharedPlaylist");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
{
b.Navigation("OwnedPlaylists");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylistEntity", b =>
{
b.Navigation("TrackAdditionLogs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,31 @@
# См. статью по ссылке https://aka.ms/customizecontainer, чтобы узнать как настроить контейнер отладки и как Visual Studio использует этот Dockerfile для создания образов для ускорения отладки.
# Этот этап используется при запуске из VS в быстром режиме (по умолчанию для конфигурации отладки)
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
# Этот этап используется для сборки проекта службы
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["nuget.config", "."]
COPY ["PlaylistShared.Api/PlaylistShared.Api.csproj", "PlaylistShared.Api/"]
RUN dotnet restore "./PlaylistShared.Api/PlaylistShared.Api.csproj"
COPY . .
WORKDIR "/src/PlaylistShared.Api"
RUN dotnet build "./PlaylistShared.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build
# Этот этап используется для публикации проекта службы, который будет скопирован на последний этап
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./PlaylistShared.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
# Этот этап используется в рабочей среде или при запуске из VS в обычном режиме (по умолчанию, когда конфигурация отладки не используется)
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "PlaylistShared.Api.dll"]

View File

@@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Identity;
namespace PlaylistShared.Api.Entities;
/// <summary>Пользователь приложения (расширяет IdentityUser).</summary>
public class ApplicationUser : IdentityUser<Guid>
{
/// <summary>Идентификатор пользователя в Яндексе (если привязан).</summary>
public string? YandexId { get; set; }
/// <summary>Access токен Яндекс.Музыки (зашифрованный).</summary>
public string? YandexAccessToken { get; set; }
/// <summary>Refresh токен Яндекс.Музыки (зашифрованный).</summary>
public string? YandexRefreshToken { get; set; }
/// <summary>Время истечения access токена Яндекса.</summary>
public DateTime YandexTokenExpiryUtc { get; set; }
/// <summary>Refresh токен для JWT (хранится в БД).</summary>
public string? RefreshToken { get; set; }
/// <summary>Время истечения refresh токена JWT.</summary>
public DateTime RefreshTokenExpiryUtc { get; set; }
/// <summary>Плейлисты, созданные пользователем.</summary>
public ICollection<SharedPlaylistEntity> OwnedPlaylists { get; set; } = new List<SharedPlaylistEntity>();
}

View File

@@ -0,0 +1,26 @@
using PlaylistShared.Shared.Enums;
namespace PlaylistShared.Api.Entities;
/// <summary>Сущность шеринг-плейлиста (таблица в БД).</summary>
public class SharedPlaylistEntity
{
public Guid Id { get; set; }
public Guid CreatorUserId { get; set; }
public string YandexPlaylistKind { get; set; } = null!;
public string YandexPlaylistOwnerUid { get; set; } = null!;
public string Title { get; set; } = null!;
public string? Description { get; set; }
public string? CoverUrl { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public bool IsDeleted { get; set; }
public string ShareToken { get; set; } = null!;
public ViewPermission ViewPermission { get; set; }
public EditPermission AddPermission { get; set; }
public EditPermission RemovePermission { get; set; }
// Навигационные свойства
public ApplicationUser Creator { get; set; } = null!;
public ICollection<TrackAdditionLogEntity> TrackAdditionLogs { get; set; } = new List<TrackAdditionLogEntity>();
}

View File

@@ -0,0 +1,15 @@
namespace PlaylistShared.Api.Entities;
/// <summary>Лог добавления трека (таблица в БД).</summary>
public class TrackAdditionLogEntity
{
public Guid Id { get; set; }
public Guid SharedPlaylistId { get; set; }
public string TrackId { get; set; } = null!;
public Guid AddedByUserId { get; set; }
public DateTime AddedAtUtc { get; set; }
// Навигационные свойства
public SharedPlaylistEntity SharedPlaylist { get; set; } = null!;
public ApplicationUser AddedByUser { get; set; } = null!;
}

View File

@@ -0,0 +1,20 @@
using System.Security.Claims;
namespace PlaylistShared.Api.Extensions;
public static class ClaimsPrincipalExtensions
{
public static Guid GetUserId(this ClaimsPrincipal user)
{
var id = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(id))
throw new UnauthorizedAccessException("User ID not found");
return Guid.Parse(id);
}
public static Guid? GetUserIdOrNull(this ClaimsPrincipal user)
{
var id = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return string.IsNullOrEmpty(id) ? null : Guid.Parse(id);
}
}

View File

@@ -0,0 +1,19 @@
using YandexMusic.API.Models.Common.Cover;
namespace PlaylistShared.Api.Extensions;
public static class YCoverExtensions
{
public static string GetUrl(this YCover cover, string size = "200x200")
{
switch (cover)
{
case YCoverImage img when !string.IsNullOrEmpty(img.Uri):
return $"https://{img.Uri.Replace("%%", size)}";
case YCoverPic pic when !string.IsNullOrEmpty(pic.Uri):
return $"https://{pic.Uri.Replace("%%", size)}";
default:
return string.Empty;
}
}
}

View File

@@ -0,0 +1,16 @@
using AutoMapper;
using PlaylistShared.Api.Entities;
using PlaylistShared.Shared.Models;
namespace PlaylistShared.Api.Mapping;
public class AppMappingProfile : Profile
{
public AppMappingProfile()
{
CreateMap<SharedPlaylistEntity, SharedPlaylistDto>()
.ForMember(dest => dest.Creator, opt => opt.MapFrom(src => src.Creator));
CreateMap<ApplicationUser, ApplicationUserDto>();
CreateMap<TrackAdditionLogEntity, TrackAdditionLogDto>();
}
}

View File

@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>a29c84f3-dccf-4a45-b139-f8effd676cd0</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="16.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
<PackageReference Include="AspNet.Security.OAuth.Yandex" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.5" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="10.1.7" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.7" />
<PackageReference Include="YandexMusic" Version="0.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PlaylistShared.Shared\PlaylistShared.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,133 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using PlaylistShared.Api.Data;
using PlaylistShared.Api.Entities;
using PlaylistShared.Api.Mapping;
using PlaylistShared.Api.Services;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
namespace PlaylistShared.Api;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
// DbContext
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));// Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole<Guid>>(options =>
{
options.User.RequireUniqueEmail = true;
options.Password.RequireDigit = false;
options.Password.RequiredLength = 6;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// JWT
var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new Exception("Jwt:Key missing");
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
})
.AddOpenIdConnect("Keycloak", options =>
{
options.Authority = builder.Configuration["Keycloak:Authority"];
options.ClientId = builder.Configuration["Keycloak:ClientId"];
options.ClientSecret = builder.Configuration["Keycloak:ClientSecret"];
options.ResponseType = "code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.CallbackPath = "/api/auth/keycloak-callback";
options.SignInScheme = IdentityConstants.ExternalScheme;
});
builder.Services.AddAuthorization();
builder.Services.AddAutoMapper(t => t.AddProfile<AppMappingProfile>());
builder.Services.AddScoped<YandexMusicService>();
builder.Services.AddScoped<SharedPlaylistService>();
builder.Services.AddScoped<TrackAdditionLogService>();
builder.Services.AddScoped<JwtService>();
builder.Services.AddDataProtection();
builder.Services.AddHttpClient();
builder.Services.AddCors(options =>
{
options.AddPolicy("Development", policy =>
{
policy.WithOrigins("http://localhost:5053", "https://localhost:7225", "http://localhost:5181", "https://api.playlistshare.frigat.duckdns.org", "https://playlistshare.frigat.duckdns.org")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
options.AddPolicy("Production", policy =>
{
policy.WithOrigins("https://api.playlistshare.frigat.duckdns.org", "https://playlistshare.frigat.duckdns.org")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddOpenApi();
var app = builder.Build();
app.MapOpenApi();
app.UseSwagger();
app.UseSwaggerUI();
if (app.Environment.IsDevelopment())
{
app.UseCors("Development");
}
else
{
app.UseHttpsRedirection();
app.UseCors("Production");
}
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}

View File

@@ -0,0 +1,31 @@
{
"profiles": {
"http": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5053"
},
"https": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7270;http://localhost:5053"
},
"Container (Dockerfile)": {
"commandName": "Docker",
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"environmentVariables": {
"ASPNETCORE_HTTPS_PORTS": "8081",
"ASPNETCORE_HTTP_PORTS": "8080"
},
"publishAllPorts": true,
"useSSL": true
}
},
"$schema": "https://json.schemastore.org/launchsettings.json"
}

View File

@@ -0,0 +1,49 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using PlaylistShared.Api.Entities;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace PlaylistShared.Api.Services;
public class JwtService
{
private readonly IConfiguration _configuration;
private readonly UserManager<ApplicationUser> _userManager;
public JwtService(IConfiguration configuration, UserManager<ApplicationUser> userManager)
{
_configuration = configuration;
_userManager = userManager;
}
public async Task<(string Token, string RefreshToken, DateTime Expiration)> GenerateTokenAsync(ApplicationUser user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.UserName!),
new Claim(ClaimTypes.Email, user.Email!),
}),
Expires = DateTime.UtcNow.AddHours(1),
Issuer = _configuration["Jwt:Issuer"],
Audience = _configuration["Jwt:Audience"],
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
var refreshToken = Guid.NewGuid().ToString();
user.RefreshToken = refreshToken;
user.RefreshTokenExpiryUtc = DateTime.UtcNow.AddDays(7);
await _userManager.UpdateAsync(user);
return (tokenString, refreshToken, tokenDescriptor.Expires.Value);
}
}

View File

@@ -0,0 +1,136 @@
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using PlaylistShared.Api.Data;
using PlaylistShared.Api.Entities;
using PlaylistShared.Shared.DTO;
using PlaylistShared.Shared.Enums;
using PlaylistShared.Shared.Models;
namespace PlaylistShared.Api.Services;
public class SharedPlaylistService
{
private readonly ApplicationDbContext _db;
private readonly IMapper _mapper;
private readonly TrackAdditionLogService _trackLogService;
public SharedPlaylistService(ApplicationDbContext db, IMapper mapper, TrackAdditionLogService trackLogService)
{
_db = db;
_mapper = mapper;
_trackLogService = trackLogService;
}
public async Task<SharedPlaylistDto> CreateAsync(Guid creatorUserId, SharePlaylistDto dto)
{
var entity = new SharedPlaylistEntity
{
Id = Guid.NewGuid(),
CreatorUserId = creatorUserId,
YandexPlaylistKind = dto.YandexPlaylistKind,
YandexPlaylistOwnerUid = dto.YandexPlaylistOwnerUid,
Title = dto.Title,
Description = dto.Description,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
ShareToken = GenerateToken(),
ViewPermission = dto.ViewPermission,
AddPermission = dto.AddPermission,
RemovePermission = dto.RemovePermission
};
_db.SharedPlaylists.Add(entity);
await _db.SaveChangesAsync();
return _mapper.Map<SharedPlaylistDto>(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 : _mapper.Map<SharedPlaylistDto>(entity);
}
public async Task<SharedPlaylistEntity?> GetEntityByTokenAsync(string token)
{
return await _db.SharedPlaylists
.Include(sp => sp.Creator)
.FirstOrDefaultAsync(sp => sp.ShareToken == token && !sp.IsDeleted);
}
public async Task<SharedPlaylistDto?> UpdatePermissionsAsync(Guid playlistId, UpdatePermissionsDto dto)
{
var entity = await _db.SharedPlaylists.FindAsync(playlistId);
if (entity == null) return null;
entity.ViewPermission = dto.ViewPermission;
entity.AddPermission = dto.AddPermission;
entity.RemovePermission = dto.RemovePermission;
entity.UpdatedAt = DateTime.UtcNow;
await _db.SaveChangesAsync();
return _mapper.Map<SharedPlaylistDto>(entity);
}
public async Task<bool> DeleteAsync(Guid playlistId)
{
var entity = await _db.SharedPlaylists.FindAsync(playlistId);
if (entity == null) return false;
entity.IsDeleted = true;
entity.UpdatedAt = DateTime.UtcNow;
await _db.SaveChangesAsync();
return true;
}
public async Task<bool> CanViewAsync(SharedPlaylistEntity playlist, Guid? currentUserId)
{
if (currentUserId == playlist.CreatorUserId) return true;
return playlist.ViewPermission == ViewPermission.Everyone ||
(playlist.ViewPermission == ViewPermission.AuthorizedOnly && currentUserId.HasValue);
}
public async Task<bool> CanAddTrackAsync(SharedPlaylistEntity playlist, Guid? currentUserId)
{
if (currentUserId == playlist.CreatorUserId) return true;
return playlist.AddPermission == EditPermission.Everyone ||
(playlist.AddPermission == EditPermission.AuthorizedOnly && currentUserId.HasValue);
}
public async Task<bool> CanRemoveTrackAsync(SharedPlaylistEntity playlist, Guid? currentUserId, string trackId)
{
if (currentUserId == playlist.CreatorUserId) return true;
return playlist.RemovePermission switch
{
EditPermission.Everyone => true,
EditPermission.AuthorizedOnly => currentUserId.HasValue,
EditPermission.AddedByUserOnly when currentUserId.HasValue =>
await _trackLogService.IsTrackAddedByUserAsync(playlist.Id, trackId, currentUserId.Value),
_ => false
};
}
public async Task<bool> IsCreatorAsync(Guid playlistId, Guid userId)
{
var playlist = await _db.SharedPlaylists.FindAsync(playlistId);
return playlist != null && playlist.CreatorUserId == userId;
}
private string GenerateToken()
{
return Convert.ToBase64String(Guid.NewGuid().ToByteArray())
.Replace("/", "_")
.Replace("+", "-")
.TrimEnd('=');
}
public async Task<SharedPlaylistEntity?> GetByYandexIdsAsync(string ownerUid, string kind)
{
return await _db.SharedPlaylists
.FirstOrDefaultAsync(sp => sp.YandexPlaylistOwnerUid == ownerUid && sp.YandexPlaylistKind == kind && !sp.IsDeleted);
}
public async Task<List<SharedPlaylistEntity>> GetAllByUserAsync(Guid userId)
{
return await _db.SharedPlaylists
.Where(sp => sp.CreatorUserId == userId && !sp.IsDeleted)
.ToListAsync();
}
}

View File

@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore;
using PlaylistShared.Api.Data;
using PlaylistShared.Api.Entities;
namespace PlaylistShared.Api.Services;
public class TrackAdditionLogService
{
private readonly ApplicationDbContext _db;
public TrackAdditionLogService(ApplicationDbContext db)
{
_db = db;
}
public async Task LogAdditionAsync(Guid sharedPlaylistId, string trackId, Guid addedByUserId)
{
var log = new TrackAdditionLogEntity
{
Id = Guid.NewGuid(),
SharedPlaylistId = sharedPlaylistId,
TrackId = trackId,
AddedByUserId = addedByUserId,
AddedAtUtc = DateTime.UtcNow
};
_db.TrackAdditionLogs.Add(log);
await _db.SaveChangesAsync();
}
public async Task<bool> IsTrackAddedByUserAsync(Guid sharedPlaylistId, string trackId, Guid userId)
{
return await _db.TrackAdditionLogs
.AnyAsync(l => l.SharedPlaylistId == sharedPlaylistId && l.TrackId == trackId && l.AddedByUserId == userId);
}
public async Task RemoveLogsForTrackAsync(Guid sharedPlaylistId, string trackId)
{
var logs = await _db.TrackAdditionLogs
.Where(l => l.SharedPlaylistId == sharedPlaylistId && l.TrackId == trackId)
.ToListAsync();
_db.TrackAdditionLogs.RemoveRange(logs);
await _db.SaveChangesAsync();
}
}

View File

@@ -0,0 +1,88 @@
using Microsoft.AspNetCore.DataProtection;
using PlaylistShared.Api.Entities;
using YandexMusic;
using YandexMusic.API.Extensions.API;
using YandexMusic.API.Models.Playlist;
namespace PlaylistShared.Api.Services;
public class YandexMusicService
{
private readonly IDataProtector _dataProtector;
public YandexMusicService(IDataProtectionProvider provider)
{
_dataProtector = provider.CreateProtector("YandexTokens");
}
private async Task<YandexMusicClient?> CreateClientAsync(ApplicationUser user)
{
if (string.IsNullOrEmpty(user.YandexAccessToken))
return null;
string decryptedToken;
try
{
decryptedToken = _dataProtector.Unprotect(user.YandexAccessToken);
}
catch
{
return null;
}
var client = new YandexMusicClient();
var success = await client.Authorize(decryptedToken);
return success ? client : null;
}
public async Task<YPlaylist?> GetPlaylistAsync(ApplicationUser user, string ownerUid, string kind)
{
var client = await CreateClientAsync(user);
if (client == null) return null;
return await client.GetPlaylistAsync(ownerUid, kind);
}
public async Task<YPlaylist?> CreatePlaylistAsync(ApplicationUser user, string title)
{
var client = await CreateClientAsync(user);
if (client == null) return null;
return await client.CreatePlaylistAsync(title);
}
public async Task<YPlaylist?> AddTracksAsync(ApplicationUser user, string ownerUid, string kind, IEnumerable<string> trackIds)
{
var client = await CreateClientAsync(user);
if (client == null) return null;
// Получаем треки по ID
var tracks = await client.GetTracksAsync(trackIds);
if (tracks == null || !tracks.Any()) return null;
var playlist = await client.GetPlaylistAsync(ownerUid, kind);
if (playlist == null) return null;
return await playlist.InsertTracksAsync(tracks.ToArray());
}
public async Task<YPlaylist?> RemoveTracksAsync(ApplicationUser user, string ownerUid, string kind, IEnumerable<string> trackIds)
{
var client = await CreateClientAsync(user);
if (client == null) return null;
var tracks = await client.GetTracksAsync(trackIds);
if (tracks == null || !tracks.Any()) return null;
var playlist = await client.GetPlaylistAsync(ownerUid, kind);
if (playlist == null) return null;
return await playlist.RemoveTracksAsync(tracks.ToArray());
}
public string EncryptToken(string token) => _dataProtector.Protect(token);
public string DecryptToken(string encryptedToken)
{
try
{
return _dataProtector.Unprotect(encryptedToken);
}
catch
{
return null;
}
}
}

View File

@@ -0,0 +1,21 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=FRIGAT-PC;Database=PlaylistShared;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=true"
},
"Jwt": {
"Key": "your-32-character-secret-key-for-jwt-minimum-length",
"Issuer": "PlaylistShared.Api",
"Audience": "PlaylistShared.Client"
},
"Yandex": {
"ClientId": "0916685f8a3641ca8fc382dbccf77236",
"ClientSecret": "f7398893cd814f8b84b85aeb2a0a6698"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,29 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=FRIGAT-PC;Database=PlaylistShared;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=true"
},
"Jwt": {
"Key": "your-32-character-secret-key-for-jwt-minimum-length",
"Issuer": "PlaylistShared.Api",
"Audience": "PlaylistShared.Client"
},
"Yandex": {
"ClientId": "your-yandex-oauth-client-id",
"ClientSecret": "your-yandex-oauth-client-secret"
},
"Client": {
"BaseUrl": "https://localhost:5002"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Keycloak": {
"Authority": "https://your-keycloak-domain/auth/realms/your-realm",
"ClientId": "playlist-shared-client",
"ClientSecret": "your-secret"
},
"AllowedHosts": "*"
}