Добавлено управление сессиями

This commit is contained in:
FrigaT
2026-04-14 01:39:25 +03:00
parent 40ea9166d2
commit fbfc6990e6
22 changed files with 1509 additions and 51 deletions

View File

@@ -13,12 +13,14 @@ public class AccountController : ControllerBase
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager; private readonly SignInManager<ApplicationUser> _signInManager;
private readonly JwtService _jwtService; private readonly JwtService _jwtService;
private readonly UserSessionService _userSessionService;
public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager, JwtService jwtService) public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager, JwtService jwtService, UserSessionService userSessionService)
{ {
_userManager = userManager; _userManager = userManager;
_signInManager = signInManager; _signInManager = signInManager;
_jwtService = jwtService; _jwtService = jwtService;
_userSessionService = userSessionService;
} }
[HttpPost("register")] [HttpPost("register")]
@@ -56,6 +58,8 @@ public class AccountController : ControllerBase
private async Task<ActionResult<ApiResponse<LoginResponse>>> GenerateTokenResponse(ApplicationUser user) private async Task<ActionResult<ApiResponse<LoginResponse>>> GenerateTokenResponse(ApplicationUser user)
{ {
await _userSessionService.GetOrCreateCurrentSessionAsync(user.Id);
var (token, refreshToken, expiration) = await _jwtService.GenerateTokenAsync(user); var (token, refreshToken, expiration) = await _jwtService.GenerateTokenAsync(user);
return Ok(ApiResponse<LoginResponse>.Ok(new LoginResponse return Ok(ApiResponse<LoginResponse>.Ok(new LoginResponse
{ {

View File

@@ -14,17 +14,20 @@ public class OpenIdController : ControllerBase
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly JwtService _jwtService; private readonly JwtService _jwtService;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly UserSessionService _userSessionService;
public OpenIdController( public OpenIdController(
SignInManager<ApplicationUser> signInManager, SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
JwtService jwtService, JwtService jwtService,
IConfiguration configuration) IConfiguration configuration,
UserSessionService userSessionService)
{ {
_signInManager = signInManager; _signInManager = signInManager;
_userManager = userManager; _userManager = userManager;
_jwtService = jwtService; _jwtService = jwtService;
_configuration = configuration; _configuration = configuration;
_userSessionService = userSessionService;
} }
[HttpGet("login")] [HttpGet("login")]
@@ -70,6 +73,7 @@ public class OpenIdController : ControllerBase
} }
await _signInManager.SignInAsync(user, isPersistent: false); await _signInManager.SignInAsync(user, isPersistent: false);
await _userSessionService.GetOrCreateCurrentSessionAsync(user.Id);
var (token, refreshToken, _) = await _jwtService.GenerateTokenAsync(user); var (token, refreshToken, _) = await _jwtService.GenerateTokenAsync(user);
return Redirect($"{_configuration["Client:BaseUrl"]}/auth-callback?token={token}&refreshToken={refreshToken}"); return Redirect($"{_configuration["Client:BaseUrl"]}/auth-callback?token={token}&refreshToken={refreshToken}");
} }

View File

@@ -16,18 +16,24 @@ public class SharedPlaylistController : ControllerBase
private readonly SharedPlaylistService _sharedService; private readonly SharedPlaylistService _sharedService;
private readonly YandexMusicService _yandexService; private readonly YandexMusicService _yandexService;
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly TrackAdditionLogService _trackLogService; private readonly UserSessionService _userSessionService;
private readonly TrackAdditionLogService _trackAdditionLogService;
private readonly TrackRemovalLogService _trackRemovalLogService;
public SharedPlaylistController( public SharedPlaylistController(
SharedPlaylistService sharedService, SharedPlaylistService sharedService,
YandexMusicService yandexService, YandexMusicService yandexService,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
TrackAdditionLogService trackLogService) TrackAdditionLogService trackAdditionLogService,
TrackRemovalLogService trackRemovalLogService,
UserSessionService userSessionService)
{ {
_sharedService = sharedService; _sharedService = sharedService;
_yandexService = yandexService; _yandexService = yandexService;
_userManager = userManager; _userManager = userManager;
_trackLogService = trackLogService; _trackAdditionLogService = trackAdditionLogService;
_trackRemovalLogService = trackRemovalLogService;
_userSessionService = userSessionService;
} }
// GET /api/sharedplaylist/{token} // GET /api/sharedplaylist/{token}
@@ -113,6 +119,13 @@ public class SharedPlaylistController : ControllerBase
if (updatedPlaylist == null) if (updatedPlaylist == null)
return StatusCode(500, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 500, Message = "Ошибка при добавлении треков" })); return StatusCode(500, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 500, Message = "Ошибка при добавлении треков" }));
var session = await _userSessionService.GetOrCreateCurrentSessionAsync(currentUserId);
var sessionId = session.SessionId;
foreach (var trackId in request.TrackIds)
{
await _trackAdditionLogService.LogAdditionAsync(playlist.Id, trackId, currentUserId, sessionId);
}
return Ok(ApiResponse<object>.Ok(new { message = "Треки добавлены" })); return Ok(ApiResponse<object>.Ok(new { message = "Треки добавлены" }));
} }
@@ -125,9 +138,12 @@ public class SharedPlaylistController : ControllerBase
if (playlist == null) if (playlist == null)
return NotFound(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" })); return NotFound(ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 404, Message = "Плейлист не найден" }));
var session = await _userSessionService.GetOrCreateCurrentSessionAsync(currentUserId);
var sessionId = session.SessionId;
foreach (var trackId in request.TrackIds) foreach (var trackId in request.TrackIds)
{ {
if (!await _sharedService.CanRemoveTrackAsync(playlist, currentUserId, trackId)) if (!await _sharedService.CanRemoveTrackAsync(playlist, currentUserId, trackId, sessionId))
return StatusCode(403, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 403, Message = $"Недостаточно прав для удаления трека {trackId}" })); return StatusCode(403, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 403, Message = $"Недостаточно прав для удаления трека {trackId}" }));
} }
@@ -140,7 +156,10 @@ public class SharedPlaylistController : ControllerBase
return StatusCode(500, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 500, Message = "Ошибка при удалении треков" })); return StatusCode(500, ApiResponse<object>.Fail(new ErrorResponse { StatusCode = 500, Message = "Ошибка при удалении треков" }));
foreach (var trackId in request.TrackIds) foreach (var trackId in request.TrackIds)
await _trackLogService.RemoveLogsForTrackAsync(playlist.Id, trackId); {
await _trackRemovalLogService.LogRemovalAsync(playlist.Id, trackId, currentUserId, sessionId);
await _trackAdditionLogService.RemoveLogsForTrackAsync(playlist.Id, trackId);
}
return Ok(ApiResponse<object>.Ok(new { message = "Треки удалены" })); return Ok(ApiResponse<object>.Ok(new { message = "Треки удалены" }));
} }

View File

@@ -9,14 +9,16 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
{ {
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
public DbSet<SharedPlaylistEntity> SharedPlaylists => Set<SharedPlaylistEntity>(); public DbSet<SharedPlaylist> SharedPlaylists => Set<SharedPlaylist>();
public DbSet<TrackAdditionLogEntity> TrackAdditionLogs => Set<TrackAdditionLogEntity>(); public DbSet<TrackAdditionLog> TrackAdditionLogs => Set<TrackAdditionLog>();
public DbSet<UserSession> UserSessions => Set<UserSession>();
public DbSet<TrackRemovalLog> TrackRemovalLogs => Set<TrackRemovalLog>();
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {
base.OnModelCreating(builder); base.OnModelCreating(builder);
builder.Entity<SharedPlaylistEntity>(entity => builder.Entity<SharedPlaylist>(entity =>
{ {
entity.HasKey(e => e.Id); entity.HasKey(e => e.Id);
entity.HasIndex(e => e.ShareToken).IsUnique(); entity.HasIndex(e => e.ShareToken).IsUnique();
@@ -29,7 +31,18 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
entity.Property(e => e.Title).IsRequired().HasMaxLength(255); entity.Property(e => e.Title).IsRequired().HasMaxLength(255);
}); });
builder.Entity<TrackAdditionLogEntity>(entity => builder.Entity<UserSession>(entity =>
{
entity.HasKey(e => e.SessionId);
entity.Property(e => e.SessionId).HasMaxLength(449);
entity.HasIndex(e => e.AssociatedUserId);
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.AssociatedUserId)
.OnDelete(DeleteBehavior.SetNull);
});
builder.Entity<TrackAdditionLog>(entity =>
{ {
entity.HasKey(e => e.Id); entity.HasKey(e => e.Id);
entity.HasIndex(e => new { e.SharedPlaylistId, e.TrackId }); entity.HasIndex(e => new { e.SharedPlaylistId, e.TrackId });
@@ -41,6 +54,28 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityR
.WithMany() .WithMany()
.HasForeignKey(e => e.AddedByUserId) .HasForeignKey(e => e.AddedByUserId)
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
entity.HasOne(e => e.Session)
.WithMany(s => s.TrackAdditionLogs)
.HasForeignKey(e => e.SessionId)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<TrackRemovalLog>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => new { e.SharedPlaylistId, e.TrackId });
entity.HasOne(e => e.SharedPlaylist)
.WithMany()
.HasForeignKey(e => e.SharedPlaylistId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.RemovedByUser)
.WithMany()
.HasForeignKey(e => e.RemovedByUserId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(e => e.Session)
.WithMany(s => s.TrackRemovalLogs)
.HasForeignKey(e => e.SessionId)
.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("20260413221546_AddSessionCacheTable")]
partial class AddSessionCacheTable
{
/// <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,32 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PlaylistShared.Api.Data.Migrations
{
/// <inheritdoc />
public partial class AddSessionCacheTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
CREATE TABLE [dbo].[SessionCache] (
[Id] NVARCHAR(449) NOT NULL,
[Value] VARBINARY(MAX) NOT NULL,
[ExpiresAtTime] DATETIMEOFFSET NOT NULL,
[SlidingExpirationInSeconds] BIGINT NULL,
[AbsoluteExpiration] DATETIMEOFFSET NULL,
CONSTRAINT [pk_SessionCache] PRIMARY KEY ([Id])
);
CREATE NONCLUSTERED INDEX [Index_ExpiresAtTime] ON [dbo].[SessionCache] ([ExpiresAtTime]);
");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("DROP TABLE [dbo].[SessionCache]");
}
}
}

View File

@@ -0,0 +1,544 @@
// <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("20260413223451_AddUserSessionsAndLogging")]
partial class AddUserSessionsAndLogging
{
/// <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.SharedPlaylist", 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.TrackAdditionLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("AddedAtUtc")
.HasColumnType("datetime2");
b.Property<Guid?>("AddedByUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("SessionId")
.IsRequired()
.HasColumnType("nvarchar(449)");
b.Property<Guid>("SharedPlaylistId")
.HasColumnType("uniqueidentifier");
b.Property<string>("TrackId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("AddedByUserId");
b.HasIndex("SessionId");
b.HasIndex("SharedPlaylistId", "TrackId");
b.ToTable("TrackAdditionLogs");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
{
b.Property<string>("SessionId")
.HasMaxLength(449)
.HasColumnType("nvarchar(449)");
b.Property<Guid?>("AssociatedUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("ClientIpAddress")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("FirstSeenUtc")
.HasColumnType("datetime2");
b.Property<DateTime>("LastSeenUtc")
.HasColumnType("datetime2");
b.Property<string>("UserAgent")
.HasColumnType("nvarchar(max)");
b.HasKey("SessionId");
b.HasIndex("AssociatedUserId");
b.ToTable("UserSessions");
});
modelBuilder.Entity("TrackRemovalLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("RemovedAtUtc")
.HasColumnType("datetime2");
b.Property<Guid?>("RemovedByUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("SessionId")
.IsRequired()
.HasColumnType("nvarchar(449)");
b.Property<Guid>("SharedPlaylistId")
.HasColumnType("uniqueidentifier");
b.Property<string>("TrackId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RemovedByUserId");
b.HasIndex("SessionId");
b.HasIndex("SharedPlaylistId", "TrackId");
b.ToTable("TrackRemovalLogs");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<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.SharedPlaylist", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
.WithMany("OwnedPlaylists")
.HasForeignKey("CreatorUserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Creator");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser")
.WithMany()
.HasForeignKey("AddedByUserId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
.WithMany("TrackAdditionLogs")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
.WithMany("TrackAdditionLogs")
.HasForeignKey("SharedPlaylistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("AddedByUser");
b.Navigation("Session");
b.Navigation("SharedPlaylist");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
.WithMany()
.HasForeignKey("AssociatedUserId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("User");
});
modelBuilder.Entity("TrackRemovalLog", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "RemovedByUser")
.WithMany()
.HasForeignKey("RemovedByUserId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
.WithMany("TrackRemovalLogs")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
.WithMany()
.HasForeignKey("SharedPlaylistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("RemovedByUser");
b.Navigation("Session");
b.Navigation("SharedPlaylist");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b =>
{
b.Navigation("OwnedPlaylists");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
{
b.Navigation("TrackAdditionLogs");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
{
b.Navigation("TrackAdditionLogs");
b.Navigation("TrackRemovalLogs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,151 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace PlaylistShared.Api.Data.Migrations
{
/// <inheritdoc />
public partial class AddUserSessionsAndLogging : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<Guid>(
name: "AddedByUserId",
table: "TrackAdditionLogs",
type: "uniqueidentifier",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uniqueidentifier");
migrationBuilder.AddColumn<string>(
name: "SessionId",
table: "TrackAdditionLogs",
type: "nvarchar(449)",
nullable: false,
defaultValue: "");
migrationBuilder.CreateTable(
name: "UserSessions",
columns: table => new
{
SessionId = table.Column<string>(type: "nvarchar(449)", maxLength: 449, nullable: false),
ClientIpAddress = table.Column<string>(type: "nvarchar(max)", nullable: true),
UserAgent = table.Column<string>(type: "nvarchar(max)", nullable: true),
FirstSeenUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
LastSeenUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
AssociatedUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UserSessions", x => x.SessionId);
table.ForeignKey(
name: "FK_UserSessions_AspNetUsers_AssociatedUserId",
column: x => x.AssociatedUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "TrackRemovalLogs",
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),
RemovedByUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
RemovedAtUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
SessionId = table.Column<string>(type: "nvarchar(449)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TrackRemovalLogs", x => x.Id);
table.ForeignKey(
name: "FK_TrackRemovalLogs_AspNetUsers_RemovedByUserId",
column: x => x.RemovedByUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_TrackRemovalLogs_SharedPlaylists_SharedPlaylistId",
column: x => x.SharedPlaylistId,
principalTable: "SharedPlaylists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_TrackRemovalLogs_UserSessions_SessionId",
column: x => x.SessionId,
principalTable: "UserSessions",
principalColumn: "SessionId",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_TrackAdditionLogs_SessionId",
table: "TrackAdditionLogs",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_TrackRemovalLogs_RemovedByUserId",
table: "TrackRemovalLogs",
column: "RemovedByUserId");
migrationBuilder.CreateIndex(
name: "IX_TrackRemovalLogs_SessionId",
table: "TrackRemovalLogs",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_TrackRemovalLogs_SharedPlaylistId_TrackId",
table: "TrackRemovalLogs",
columns: new[] { "SharedPlaylistId", "TrackId" });
migrationBuilder.CreateIndex(
name: "IX_UserSessions_AssociatedUserId",
table: "UserSessions",
column: "AssociatedUserId");
migrationBuilder.AddForeignKey(
name: "FK_TrackAdditionLogs_UserSessions_SessionId",
table: "TrackAdditionLogs",
column: "SessionId",
principalTable: "UserSessions",
principalColumn: "SessionId",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_TrackAdditionLogs_UserSessions_SessionId",
table: "TrackAdditionLogs");
migrationBuilder.DropTable(
name: "TrackRemovalLogs");
migrationBuilder.DropTable(
name: "UserSessions");
migrationBuilder.DropIndex(
name: "IX_TrackAdditionLogs_SessionId",
table: "TrackAdditionLogs");
migrationBuilder.DropColumn(
name: "SessionId",
table: "TrackAdditionLogs");
migrationBuilder.AlterColumn<Guid>(
name: "AddedByUserId",
table: "TrackAdditionLogs",
type: "uniqueidentifier",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
oldClrType: typeof(Guid),
oldType: "uniqueidentifier",
oldNullable: true);
}
}
}

View File

@@ -237,7 +237,7 @@ namespace PlaylistShared.Api.Data.Migrations
b.ToTable("AspNetUsers", (string)null); b.ToTable("AspNetUsers", (string)null);
}); });
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylistEntity", b => modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -299,7 +299,7 @@ namespace PlaylistShared.Api.Data.Migrations
b.ToTable("SharedPlaylists"); b.ToTable("SharedPlaylists");
}); });
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLogEntity", b => modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -308,9 +308,13 @@ namespace PlaylistShared.Api.Data.Migrations
b.Property<DateTime>("AddedAtUtc") b.Property<DateTime>("AddedAtUtc")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
b.Property<Guid>("AddedByUserId") b.Property<Guid?>("AddedByUserId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<string>("SessionId")
.IsRequired()
.HasColumnType("nvarchar(449)");
b.Property<Guid>("SharedPlaylistId") b.Property<Guid>("SharedPlaylistId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
@@ -322,11 +326,75 @@ namespace PlaylistShared.Api.Data.Migrations
b.HasIndex("AddedByUserId"); b.HasIndex("AddedByUserId");
b.HasIndex("SessionId");
b.HasIndex("SharedPlaylistId", "TrackId"); b.HasIndex("SharedPlaylistId", "TrackId");
b.ToTable("TrackAdditionLogs"); b.ToTable("TrackAdditionLogs");
}); });
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
{
b.Property<string>("SessionId")
.HasMaxLength(449)
.HasColumnType("nvarchar(449)");
b.Property<Guid?>("AssociatedUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("ClientIpAddress")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("FirstSeenUtc")
.HasColumnType("datetime2");
b.Property<DateTime>("LastSeenUtc")
.HasColumnType("datetime2");
b.Property<string>("UserAgent")
.HasColumnType("nvarchar(max)");
b.HasKey("SessionId");
b.HasIndex("AssociatedUserId");
b.ToTable("UserSessions");
});
modelBuilder.Entity("TrackRemovalLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("RemovedAtUtc")
.HasColumnType("datetime2");
b.Property<Guid?>("RemovedByUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("SessionId")
.IsRequired()
.HasColumnType("nvarchar(449)");
b.Property<Guid>("SharedPlaylistId")
.HasColumnType("uniqueidentifier");
b.Property<string>("TrackId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RemovedByUserId");
b.HasIndex("SessionId");
b.HasIndex("SharedPlaylistId", "TrackId");
b.ToTable("TrackRemovalLogs");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{ {
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null) b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole<System.Guid>", null)
@@ -378,7 +446,7 @@ namespace PlaylistShared.Api.Data.Migrations
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylistEntity", b => modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
{ {
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator") b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "Creator")
.WithMany("OwnedPlaylists") .WithMany("OwnedPlaylists")
@@ -389,15 +457,20 @@ namespace PlaylistShared.Api.Data.Migrations
b.Navigation("Creator"); b.Navigation("Creator");
}); });
modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLogEntity", b => modelBuilder.Entity("PlaylistShared.Api.Entities.TrackAdditionLog", b =>
{ {
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser") b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "AddedByUser")
.WithMany() .WithMany()
.HasForeignKey("AddedByUserId") .HasForeignKey("AddedByUserId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
.WithMany("TrackAdditionLogs")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Restrict) .OnDelete(DeleteBehavior.Restrict)
.IsRequired(); .IsRequired();
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylistEntity", "SharedPlaylist") b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
.WithMany("TrackAdditionLogs") .WithMany("TrackAdditionLogs")
.HasForeignKey("SharedPlaylistId") .HasForeignKey("SharedPlaylistId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -405,6 +478,44 @@ namespace PlaylistShared.Api.Data.Migrations
b.Navigation("AddedByUser"); b.Navigation("AddedByUser");
b.Navigation("Session");
b.Navigation("SharedPlaylist");
});
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "User")
.WithMany()
.HasForeignKey("AssociatedUserId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("User");
});
modelBuilder.Entity("TrackRemovalLog", b =>
{
b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", "RemovedByUser")
.WithMany()
.HasForeignKey("RemovedByUserId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("PlaylistShared.Api.Entities.UserSession", "Session")
.WithMany("TrackRemovalLogs")
.HasForeignKey("SessionId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("PlaylistShared.Api.Entities.SharedPlaylist", "SharedPlaylist")
.WithMany()
.HasForeignKey("SharedPlaylistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("RemovedByUser");
b.Navigation("Session");
b.Navigation("SharedPlaylist"); b.Navigation("SharedPlaylist");
}); });
@@ -413,10 +524,17 @@ namespace PlaylistShared.Api.Data.Migrations
b.Navigation("OwnedPlaylists"); b.Navigation("OwnedPlaylists");
}); });
modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylistEntity", b => modelBuilder.Entity("PlaylistShared.Api.Entities.SharedPlaylist", b =>
{ {
b.Navigation("TrackAdditionLogs"); b.Navigation("TrackAdditionLogs");
}); });
modelBuilder.Entity("PlaylistShared.Api.Entities.UserSession", b =>
{
b.Navigation("TrackAdditionLogs");
b.Navigation("TrackRemovalLogs");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@@ -24,5 +24,5 @@ public class ApplicationUser : IdentityUser<Guid>
public DateTime RefreshTokenExpiryUtc { get; set; } public DateTime RefreshTokenExpiryUtc { get; set; }
/// <summary>Плейлисты, созданные пользователем.</summary> /// <summary>Плейлисты, созданные пользователем.</summary>
public ICollection<SharedPlaylistEntity> OwnedPlaylists { get; set; } = new List<SharedPlaylistEntity>(); public ICollection<SharedPlaylist> OwnedPlaylists { get; set; } = new List<SharedPlaylist>();
} }

View File

@@ -2,8 +2,8 @@
namespace PlaylistShared.Api.Entities; namespace PlaylistShared.Api.Entities;
/// <summary>Сущность шеринг-плейлиста (таблица в БД).</summary> /// <summary>Сущность шеринг-плейлиста.</summary>
public class SharedPlaylistEntity public class SharedPlaylist
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid CreatorUserId { get; set; } public Guid CreatorUserId { get; set; }
@@ -22,5 +22,5 @@ public class SharedPlaylistEntity
// Навигационные свойства // Навигационные свойства
public ApplicationUser Creator { get; set; } = null!; public ApplicationUser Creator { get; set; } = null!;
public ICollection<TrackAdditionLogEntity> TrackAdditionLogs { get; set; } = new List<TrackAdditionLogEntity>(); public ICollection<TrackAdditionLog> TrackAdditionLogs { get; set; } = new List<TrackAdditionLog>();
} }

View File

@@ -0,0 +1,16 @@
namespace PlaylistShared.Api.Entities;
/// <summary>Лог добавления трека.</summary>
public class TrackAdditionLog
{
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 string SessionId { get; set; } = null!;
public SharedPlaylist SharedPlaylist { get; set; } = null!;
public ApplicationUser? AddedByUser { get; set; }
public UserSession Session { get; set; } = null!;
}

View File

@@ -1,15 +0,0 @@
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,15 @@
using PlaylistShared.Api.Entities;
public class TrackRemovalLog
{
public Guid Id { get; set; }
public Guid SharedPlaylistId { get; set; }
public string TrackId { get; set; } = null!;
public Guid? RemovedByUserId { get; set; }
public DateTime RemovedAtUtc { get; set; }
public string SessionId { get; set; } = null!;
public SharedPlaylist SharedPlaylist { get; set; } = null!;
public ApplicationUser? RemovedByUser { get; set; }
public UserSession Session { get; set; } = null!;
}

View File

@@ -0,0 +1,15 @@
namespace PlaylistShared.Api.Entities;
public class UserSession
{
public string SessionId { get; set; } = null!; // HttpContext.Session.Id
public string? ClientIpAddress { get; set; }
public string? UserAgent { get; set; }
public DateTime FirstSeenUtc { get; set; }
public DateTime LastSeenUtc { get; set; }
public Guid? AssociatedUserId { get; set; } // если позже залогинился
public ApplicationUser? User { get; set; }
public ICollection<TrackAdditionLog> TrackAdditionLogs { get; set; } = new List<TrackAdditionLog>();
public ICollection<TrackRemovalLog> TrackRemovalLogs { get; set; } = new List<TrackRemovalLog>();
}

View File

@@ -9,7 +9,7 @@ public class AppMappingProfile : Profile
{ {
public AppMappingProfile() public AppMappingProfile()
{ {
CreateMap<SharedPlaylistEntity, SharedPlaylistDto>() CreateMap<SharedPlaylist, SharedPlaylistDto>()
.ForMember(dest => dest.Creator, opt => opt.MapFrom(src => src.Creator)); .ForMember(dest => dest.Creator, opt => opt.MapFrom(src => src.Creator));
CreateMap<ApplicationUser, ApplicationUserDto>(); CreateMap<ApplicationUser, ApplicationUserDto>();
} }

View File

@@ -22,6 +22,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="10.0.5" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="10.1.7" /> <PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="10.1.7" />

View File

@@ -35,6 +35,22 @@ public class Program
.AddEntityFrameworkStores<ApplicationDbContext>() .AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders(); .AddDefaultTokenProviders();
// Session
builder.Services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = builder.Configuration.GetConnectionString("DefaultConnection");
options.SchemaName = "dbo";
options.TableName = "SessionCache";
});
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromDays(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
options.Cookie.SameSite = SameSiteMode.Lax;
});
// JWT // JWT
var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new Exception("Jwt:Key missing"); var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new Exception("Jwt:Key missing");
builder.Services.AddAuthentication(options => builder.Services.AddAuthentication(options =>
@@ -70,12 +86,15 @@ public class Program
options.SignInScheme = IdentityConstants.ExternalScheme; options.SignInScheme = IdentityConstants.ExternalScheme;
}); });
builder.Services.AddHttpContextAccessor();
builder.Services.AddAuthorization(); builder.Services.AddAuthorization();
builder.Services.AddAutoMapper(t => t.AddProfile<AppMappingProfile>()); builder.Services.AddAutoMapper(t => t.AddProfile<AppMappingProfile>());
builder.Services.AddScoped<JwtService>();
builder.Services.AddScoped<UserSessionService>();
builder.Services.AddScoped<YandexMusicService>(); builder.Services.AddScoped<YandexMusicService>();
builder.Services.AddScoped<SharedPlaylistService>(); builder.Services.AddScoped<SharedPlaylistService>();
builder.Services.AddScoped<TrackAdditionLogService>(); builder.Services.AddScoped<TrackAdditionLogService>();
builder.Services.AddScoped<JwtService>(); builder.Services.AddScoped<TrackRemovalLogService>();
builder.Services.AddDataProtection(); builder.Services.AddDataProtection();
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
@@ -123,6 +142,7 @@ public class Program
app.UseCors("Production"); app.UseCors("Production");
} }
app.UseSession();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();

View File

@@ -23,7 +23,7 @@ public class SharedPlaylistService
public async Task<SharedPlaylistDto> CreateAsync(Guid creatorUserId, SharePlaylistDto dto) public async Task<SharedPlaylistDto> CreateAsync(Guid creatorUserId, SharePlaylistDto dto)
{ {
var entity = new SharedPlaylistEntity var entity = new SharedPlaylist
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
CreatorUserId = creatorUserId, CreatorUserId = creatorUserId,
@@ -51,7 +51,7 @@ public class SharedPlaylistService
return entity == null ? null : _mapper.Map<SharedPlaylistDto>(entity); return entity == null ? null : _mapper.Map<SharedPlaylistDto>(entity);
} }
public async Task<SharedPlaylistEntity?> GetEntityByTokenAsync(string token) public async Task<SharedPlaylist?> GetEntityByTokenAsync(string token)
{ {
return await _db.SharedPlaylists return await _db.SharedPlaylists
.Include(sp => sp.Creator) .Include(sp => sp.Creator)
@@ -80,21 +80,21 @@ public class SharedPlaylistService
return true; return true;
} }
public async Task<bool> CanViewAsync(SharedPlaylistEntity playlist, Guid? currentUserId) public async Task<bool> CanViewAsync(SharedPlaylist playlist, Guid? currentUserId)
{ {
if (currentUserId == playlist.CreatorUserId) return true; if (currentUserId == playlist.CreatorUserId) return true;
return playlist.ViewPermission == ViewPermission.Everyone || return playlist.ViewPermission == ViewPermission.Everyone ||
(playlist.ViewPermission == ViewPermission.AuthorizedOnly && currentUserId.HasValue); (playlist.ViewPermission == ViewPermission.AuthorizedOnly && currentUserId.HasValue);
} }
public async Task<bool> CanAddTrackAsync(SharedPlaylistEntity playlist, Guid? currentUserId) public async Task<bool> CanAddTrackAsync(SharedPlaylist playlist, Guid? currentUserId)
{ {
if (currentUserId == playlist.CreatorUserId) return true; if (currentUserId == playlist.CreatorUserId) return true;
return playlist.AddPermission == EditPermission.Everyone || return playlist.AddPermission == EditPermission.Everyone ||
(playlist.AddPermission == EditPermission.AuthorizedOnly && currentUserId.HasValue); (playlist.AddPermission == EditPermission.AuthorizedOnly && currentUserId.HasValue);
} }
public async Task<bool> CanRemoveTrackAsync(SharedPlaylistEntity playlist, Guid? currentUserId, string trackId) public async Task<bool> CanRemoveTrackAsync(SharedPlaylist playlist, Guid? currentUserId, string trackId, string sessionId)
{ {
if (currentUserId == playlist.CreatorUserId) return true; if (currentUserId == playlist.CreatorUserId) return true;
return playlist.RemovePermission switch return playlist.RemovePermission switch
@@ -102,7 +102,9 @@ public class SharedPlaylistService
EditPermission.Everyone => true, EditPermission.Everyone => true,
EditPermission.AuthorizedOnly => currentUserId.HasValue, EditPermission.AuthorizedOnly => currentUserId.HasValue,
EditPermission.AddedByUserOnly when currentUserId.HasValue => EditPermission.AddedByUserOnly when currentUserId.HasValue =>
await _trackLogService.IsTrackAddedByUserAsync(playlist.Id, trackId, currentUserId.Value), await _trackLogService.IsTrackAddedByCurrentUserOrSessionAsync(playlist.Id, trackId, currentUserId, sessionId),
EditPermission.AddedByUserOnly when !currentUserId.HasValue =>
await _trackLogService.IsTrackAddedByCurrentUserOrSessionAsync(playlist.Id, trackId, null, sessionId),
_ => false _ => false
}; };
} }
@@ -121,7 +123,7 @@ public class SharedPlaylistService
.TrimEnd('='); .TrimEnd('=');
} }
public async Task<List<SharedPlaylistEntity>> GetAllByUserAsync(Guid userId) public async Task<List<SharedPlaylist>> GetAllByUserAsync(Guid userId)
{ {
return await _db.SharedPlaylists return await _db.SharedPlaylists
.Where(sp => sp.CreatorUserId == userId && !sp.IsDeleted) .Where(sp => sp.CreatorUserId == userId && !sp.IsDeleted)

View File

@@ -13,24 +13,26 @@ public class TrackAdditionLogService
_db = db; _db = db;
} }
public async Task LogAdditionAsync(Guid sharedPlaylistId, string trackId, Guid addedByUserId) public async Task LogAdditionAsync(Guid sharedPlaylistId, string trackId, Guid? addedByUserId, string sessionId)
{ {
var log = new TrackAdditionLogEntity var log = new TrackAdditionLog
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
SharedPlaylistId = sharedPlaylistId, SharedPlaylistId = sharedPlaylistId,
TrackId = trackId, TrackId = trackId,
AddedByUserId = addedByUserId, AddedByUserId = addedByUserId,
AddedAtUtc = DateTime.UtcNow AddedAtUtc = DateTime.UtcNow,
SessionId = sessionId
}; };
_db.TrackAdditionLogs.Add(log); _db.TrackAdditionLogs.Add(log);
await _db.SaveChangesAsync(); await _db.SaveChangesAsync();
} }
public async Task<bool> IsTrackAddedByUserAsync(Guid sharedPlaylistId, string trackId, Guid userId) public async Task<bool> IsTrackAddedByCurrentUserOrSessionAsync(Guid sharedPlaylistId, string trackId, Guid? userId, string sessionId)
{ {
return await _db.TrackAdditionLogs return await _db.TrackAdditionLogs
.AnyAsync(l => l.SharedPlaylistId == sharedPlaylistId && l.TrackId == trackId && l.AddedByUserId == userId); .AnyAsync(l => l.SharedPlaylistId == sharedPlaylistId && l.TrackId == trackId &&
(userId != null ? l.AddedByUserId == userId : l.SessionId == sessionId));
} }
public async Task RemoveLogsForTrackAsync(Guid sharedPlaylistId, string trackId) public async Task RemoveLogsForTrackAsync(Guid sharedPlaylistId, string trackId)

View File

@@ -0,0 +1,22 @@
using PlaylistShared.Api.Data;
public class TrackRemovalLogService
{
private readonly ApplicationDbContext _db;
public TrackRemovalLogService(ApplicationDbContext db) => _db = db;
public async Task LogRemovalAsync(Guid sharedPlaylistId, string trackId, Guid? removedByUserId, string sessionId)
{
var log = new TrackRemovalLog
{
Id = Guid.NewGuid(),
SharedPlaylistId = sharedPlaylistId,
TrackId = trackId,
RemovedByUserId = removedByUserId,
RemovedAtUtc = DateTime.UtcNow,
SessionId = sessionId
};
_db.TrackRemovalLogs.Add(log);
await _db.SaveChangesAsync();
}
}

View File

@@ -0,0 +1,47 @@
using PlaylistShared.Api.Data;
using PlaylistShared.Api.Entities;
public class UserSessionService
{
private readonly ApplicationDbContext _db;
private readonly IHttpContextAccessor _httpContextAccessor;
public UserSessionService(ApplicationDbContext db, IHttpContextAccessor httpContextAccessor)
{
_db = db;
_httpContextAccessor = httpContextAccessor;
}
public async Task<UserSession> GetOrCreateCurrentSessionAsync(Guid? associatedUserId = null)
{
var httpContext = _httpContextAccessor.HttpContext
?? throw new InvalidOperationException("No HttpContext available");
var sessionId = httpContext.Session.Id;
var now = DateTime.UtcNow;
var session = await _db.UserSessions.FindAsync(sessionId);
if (session == null)
{
session = new UserSession
{
SessionId = sessionId,
ClientIpAddress = httpContext.Connection.RemoteIpAddress?.ToString(),
UserAgent = httpContext.Request.Headers["User-Agent"].ToString(),
FirstSeenUtc = now,
LastSeenUtc = now,
AssociatedUserId = associatedUserId
};
_db.UserSessions.Add(session);
}
else
{
session.LastSeenUtc = now;
if (session.AssociatedUserId == null && associatedUserId != null)
session.AssociatedUserId = associatedUserId;
_db.UserSessions.Update(session);
}
await _db.SaveChangesAsync();
return session;
}
}