From 164cf455fdde9fd0ecc25a20fc29e14162d8129c Mon Sep 17 00:00:00 2001 From: FrigaT Date: Tue, 14 Apr 2026 13:11:34 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B0=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B2=D0=BE=D1=81=D0=BF=D1=80=D0=BE=D0=B8=D0=B7=D0=B2=D0=B5?= =?UTF-8?q?=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/AudioController.cs | 43 +- .../Controllers/PlaylistsController.cs | 3 +- ...414094124_AddSharedPermissions.Designer.cs | 547 ++++++++++++++++++ .../20260414094124_AddSharedPermissions.cs | 29 + .../ApplicationDbContextModelSnapshot.cs | 3 + PlaylistShared.Api/Entities/SharedPlaylist.cs | 1 + PlaylistShared.Api/Program.cs | 24 +- .../Services/SharedPlaylistService.cs | 14 + PlaylistShared.Api/appsettings.json | 6 + .../Components/AudioPlayer.razor | 9 +- .../Pages/SharedPlaylistView.razor | 92 ++- PlaylistShared.Pwa/wwwroot/index.html | 68 --- PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js | 3 +- .../Playlist/SharePlaylistDto.cs | 4 + .../Shared/SharedPlaylistDto.cs | 4 + .../Shared/UpdatePermissionsDto.cs | 10 +- 16 files changed, 724 insertions(+), 136 deletions(-) create mode 100644 PlaylistShared.Api/Data/Migrations/20260414094124_AddSharedPermissions.Designer.cs create mode 100644 PlaylistShared.Api/Data/Migrations/20260414094124_AddSharedPermissions.cs diff --git a/PlaylistShared.Api/Controllers/AudioController.cs b/PlaylistShared.Api/Controllers/AudioController.cs index d2afe87..8611e3a 100644 --- a/PlaylistShared.Api/Controllers/AudioController.cs +++ b/PlaylistShared.Api/Controllers/AudioController.cs @@ -13,15 +13,18 @@ public class AudioController : ControllerBase { private readonly UserManager _userManager; private readonly YandexMusicService _yandexService; + private readonly SharedPlaylistService _sharedService; private readonly JwtService _jwtService; public AudioController( UserManager userManager, YandexMusicService yandexService, + SharedPlaylistService sharedService, JwtService jwtService) { _userManager = userManager; _yandexService = yandexService; + _sharedService = sharedService; _jwtService = jwtService; } @@ -29,17 +32,18 @@ public class AudioController : ControllerBase /// Потоковое воспроизведение трека из Яндекс.Музыки. /// /// ID трека (например, "21696942"). + /// gwt пользователя + /// ID расшаренного плейлиста [HttpGet("track/{trackId}")] [AllowAnonymous] - public async Task StreamTrack(string trackId, [FromQuery] string? access_token = null) + public async Task StreamTrack(string trackId, [FromQuery] string? access_token = null, [FromQuery] string? shared_id = null) { var user = await GetUserFromToken(access_token); - if (user == null) - return Unauthorized(); + if (user == null) user = await GetUserFromSharedPlaylistId(shared_id); + if (user == null) return Unauthorized(); var streamUrl = await _yandexService.GetTrackFileUrlAsync(user, trackId); - if (string.IsNullOrEmpty(streamUrl)) - return NotFound(); + if (string.IsNullOrEmpty(streamUrl)) return NotFound(); var httpClient = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Get, streamUrl); @@ -56,11 +60,11 @@ public class AudioController : ControllerBase Response.ContentType = response.Content.Headers.ContentType?.ToString() ?? "audio/mpeg"; if (response.Content.Headers.Contains("Content-Range")) - Response.Headers.Add("Content-Range", response.Content.Headers.ContentRange?.ToString()); + Response.Headers.Append("Content-Range", response.Content.Headers.ContentRange?.ToString()); if (response.Headers.Contains("Accept-Ranges")) - Response.Headers.Add("Accept-Ranges", response.Headers.AcceptRanges?.ToString()); + Response.Headers.Append("Accept-Ranges", response.Headers.AcceptRanges?.ToString()); if (response.Content.Headers.Contains("Content-Length")) - Response.Headers.Add("Content-Length", response.Content.Headers.ContentLength?.ToString()); + Response.Headers.Append("Content-Length", response.Content.Headers.ContentLength?.ToString()); await response.Content.CopyToAsync(Response.Body); return new EmptyResult(); @@ -68,17 +72,28 @@ public class AudioController : ControllerBase private async Task GetUserFromToken(string? token) { - if (string.IsNullOrEmpty(token)) - return null; + if (string.IsNullOrEmpty(token)) return null; var principal = _jwtService.ValidateToken(token); - if (principal == null) - return null; + if (principal == null) return null; var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(userId)) - return null; + if (string.IsNullOrEmpty(userId)) return null; return await _userManager.FindByIdAsync(userId); } + + private async Task GetUserFromSharedPlaylistId(string? sharedId) + { + if (string.IsNullOrEmpty(sharedId)) return null; + + var playlist = await _sharedService.GetEntityByTokenAsync(sharedId); + + if (playlist == null) return null; + + if (!await _sharedService.CanPlayEveryoneAsync(playlist)) return null; + + return await _userManager.FindByIdAsync(playlist.CreatorUserId.ToString()); + + } } \ No newline at end of file diff --git a/PlaylistShared.Api/Controllers/PlaylistsController.cs b/PlaylistShared.Api/Controllers/PlaylistsController.cs index 39aa7ce..823f25a 100644 --- a/PlaylistShared.Api/Controllers/PlaylistsController.cs +++ b/PlaylistShared.Api/Controllers/PlaylistsController.cs @@ -87,8 +87,9 @@ public class PlaylistsController : ControllerBase Title = playlist.Title, Description = playlist.Description, ViewPermission = ViewPermission.Everyone, + PlayPermission = ViewPermission.Everyone, AddPermission = EditPermission.AuthorizedOnly, - RemovePermission = EditPermission.AddedByUserOnly + RemovePermission = EditPermission.AddedByUserOnly, }; var result = await _sharedService.CreateAsync(userId, dto); diff --git a/PlaylistShared.Api/Data/Migrations/20260414094124_AddSharedPermissions.Designer.cs b/PlaylistShared.Api/Data/Migrations/20260414094124_AddSharedPermissions.Designer.cs new file mode 100644 index 0000000..178fd2e --- /dev/null +++ b/PlaylistShared.Api/Data/Migrations/20260414094124_AddSharedPermissions.Designer.cs @@ -0,0 +1,547 @@ +// +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("20260414094124_AddSharedPermissions")] + partial class AddSharedPermissions + { + /// + 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", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("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", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("PlaylistShared.Api.Entities.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("RefreshToken") + .HasColumnType("nvarchar(max)"); + + b.Property("RefreshTokenExpiryUtc") + .HasColumnType("datetime2"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("YandexAccessToken") + .HasColumnType("nvarchar(max)"); + + b.Property("YandexId") + .HasColumnType("nvarchar(max)"); + + b.Property("YandexRefreshToken") + .HasColumnType("nvarchar(max)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddPermission") + .HasColumnType("int"); + + b.Property("CoverUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatorUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PlayPermission") + .HasColumnType("int"); + + b.Property("RemovePermission") + .HasColumnType("int"); + + b.Property("ShareToken") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("ViewPermission") + .HasColumnType("int"); + + b.Property("YandexPlaylistKind") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AddedAtUtc") + .HasColumnType("datetime2"); + + b.Property("AddedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(449)"); + + b.Property("SharedPlaylistId") + .HasColumnType("uniqueidentifier"); + + b.Property("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("SessionId") + .HasMaxLength(449) + .HasColumnType("nvarchar(449)"); + + b.Property("AssociatedUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("ClientIpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("FirstSeenUtc") + .HasColumnType("datetime2"); + + b.Property("LastSeenUtc") + .HasColumnType("datetime2"); + + b.Property("UserAgent") + .HasColumnType("nvarchar(max)"); + + b.HasKey("SessionId"); + + b.HasIndex("AssociatedUserId"); + + b.ToTable("UserSessions"); + }); + + modelBuilder.Entity("TrackRemovalLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("RemovedAtUtc") + .HasColumnType("datetime2"); + + b.Property("RemovedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(449)"); + + b.Property("SharedPlaylistId") + .HasColumnType("uniqueidentifier"); + + b.Property("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", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("PlaylistShared.Api.Entities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", 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", 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 + } + } +} diff --git a/PlaylistShared.Api/Data/Migrations/20260414094124_AddSharedPermissions.cs b/PlaylistShared.Api/Data/Migrations/20260414094124_AddSharedPermissions.cs new file mode 100644 index 0000000..7d34f6c --- /dev/null +++ b/PlaylistShared.Api/Data/Migrations/20260414094124_AddSharedPermissions.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PlaylistShared.Api.Data.Migrations +{ + /// + public partial class AddSharedPermissions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PlayPermission", + table: "SharedPlaylists", + type: "int", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PlayPermission", + table: "SharedPlaylists"); + } + } +} diff --git a/PlaylistShared.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/PlaylistShared.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 26f5e59..9811f3c 100644 --- a/PlaylistShared.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/PlaylistShared.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -261,6 +261,9 @@ namespace PlaylistShared.Api.Data.Migrations b.Property("IsDeleted") .HasColumnType("bit"); + b.Property("PlayPermission") + .HasColumnType("int"); + b.Property("RemovePermission") .HasColumnType("int"); diff --git a/PlaylistShared.Api/Entities/SharedPlaylist.cs b/PlaylistShared.Api/Entities/SharedPlaylist.cs index 5ca053a..2684db7 100644 --- a/PlaylistShared.Api/Entities/SharedPlaylist.cs +++ b/PlaylistShared.Api/Entities/SharedPlaylist.cs @@ -17,6 +17,7 @@ public class SharedPlaylist public bool IsDeleted { get; set; } public string ShareToken { get; set; } = null!; public ViewPermission ViewPermission { get; set; } + public ViewPermission PlayPermission { get; set; } public EditPermission AddPermission { get; set; } public EditPermission RemovePermission { get; set; } diff --git a/PlaylistShared.Api/Program.cs b/PlaylistShared.Api/Program.cs index d0acef5..820573f 100644 --- a/PlaylistShared.Api/Program.cs +++ b/PlaylistShared.Api/Program.cs @@ -23,7 +23,8 @@ public class Program // DbContext builder.Services.AddDbContext(options => - options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));// Identity + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + // Identity builder.Services.AddIdentity>(options => { options.User.RequireUniqueEmail = true; @@ -101,17 +102,9 @@ public class Program 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") + policy.WithOrigins(builder.Configuration.GetSection("Cors:Origins").Get()) .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); @@ -131,17 +124,14 @@ public class Program app.UseSwagger(); app.UseSwaggerUI(); - if (app.Environment.IsDevelopment()) - { - app.UseCors("Development"); - } - else - { + app.UseCors("Production"); + if (!app.Environment.IsDevelopment()) + { app.UseHttpsRedirection(); - app.UseCors("Production"); } + app.UseSession(); app.UseAuthentication(); app.UseAuthorization(); diff --git a/PlaylistShared.Api/Services/SharedPlaylistService.cs b/PlaylistShared.Api/Services/SharedPlaylistService.cs index 7d529ff..a9b157c 100644 --- a/PlaylistShared.Api/Services/SharedPlaylistService.cs +++ b/PlaylistShared.Api/Services/SharedPlaylistService.cs @@ -34,6 +34,7 @@ public class SharedPlaylistService CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, ShareToken = GenerateToken(), + PlayPermission = dto.PlayPermission, ViewPermission = dto.ViewPermission, AddPermission = dto.AddPermission, RemovePermission = dto.RemovePermission @@ -63,6 +64,7 @@ public class SharedPlaylistService var entity = await _db.SharedPlaylists.FindAsync(playlistId); if (entity == null) return null; entity.ViewPermission = dto.ViewPermission; + entity.PlayPermission = dto.PlayPermission; entity.AddPermission = dto.AddPermission; entity.RemovePermission = dto.RemovePermission; entity.UpdatedAt = DateTime.UtcNow; @@ -80,6 +82,18 @@ public class SharedPlaylistService return true; } + public async Task CanPlayAsync(SharedPlaylist playlist, Guid? currentUserId) + { + if (currentUserId == playlist.CreatorUserId) return true; + return playlist.PlayPermission == ViewPermission.Everyone || + (playlist.PlayPermission == ViewPermission.AuthorizedOnly && currentUserId.HasValue); + } + + public async Task CanPlayEveryoneAsync(SharedPlaylist playlist) + { + return playlist.PlayPermission == ViewPermission.Everyone; + } + public async Task CanViewAsync(SharedPlaylist playlist, Guid? currentUserId) { if (currentUserId == playlist.CreatorUserId) return true; diff --git a/PlaylistShared.Api/appsettings.json b/PlaylistShared.Api/appsettings.json index e768460..117667c 100644 --- a/PlaylistShared.Api/appsettings.json +++ b/PlaylistShared.Api/appsettings.json @@ -7,6 +7,12 @@ "Issuer": "PlaylistShared.Api", "Audience": "PlaylistShared.Client" }, + "Cors": { + "Origins": [ + "https://api.playlistshare.frigat.duckdns.org", + "https://playlistshare.frigat.duckdns.org" + ] + }, "Yandex": { "ClientId": "0916685f8a3641ca8fc382dbccf77236", "ClientSecret": "f7398893cd814f8b84b85aeb2a0a6698" diff --git a/PlaylistShared.Pwa/Components/AudioPlayer.razor b/PlaylistShared.Pwa/Components/AudioPlayer.razor index b750e4e..d2be21d 100644 --- a/PlaylistShared.Pwa/Components/AudioPlayer.razor +++ b/PlaylistShared.Pwa/Components/AudioPlayer.razor @@ -65,6 +65,9 @@ /// Требовать ли авторизацию для воспроизведения (по умолчанию true). [Parameter] public bool RequireAuth { get; set; } = true; + /// ID расшаренного плейлиста. + [Parameter] public string SharedPlaylistId { get; set; } = string.Empty; + /// Событие при завершении трека. [Parameter] public EventCallback OnTrackEnded { get; set; } @@ -139,16 +142,16 @@ var tokens = await TokenStorage.GetTokensAsync(); var accessToken = tokens.token; - if (string.IsNullOrEmpty(accessToken)) + if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(SharedPlaylistId)) { Snackbar.Add("Токен авторизации не найден. Пожалуйста, войдите заново.", Severity.Error); return; } - var streamUrl = new Uri(Http.BaseAddress, $"/api/audio/track/{trackId}").ToString(); + var streamUrl = new Uri(Http.BaseAddress!, $"/api/audio/track/{trackId}").ToString(); await EnsureAudioModuleAsync(); - await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, accessToken); + await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, accessToken, SharedPlaylistId); _isPlaying = true; StartProgressTimer(); StateHasChanged(); diff --git a/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor b/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor index b4aeb79..3c572c3 100644 --- a/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor +++ b/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor @@ -50,6 +50,12 @@ Только авторизованные + + + Все + Только авторизованные + + Все @@ -144,11 +150,19 @@ @if (!string.IsNullOrEmpty(context.CoverUri)) { - + @if (@_canPlay) + { + + } + else + { + + + } } @@ -174,7 +188,7 @@
- +
@@ -191,6 +205,7 @@ private bool _loading = true; private bool _isAuthenticated; private bool _isCreator; + private bool _canPlay; private bool _canAdd; private bool _canRemove; private UpdatePermissionsDto _editPermissions = new(); @@ -211,6 +226,45 @@ await LoadPlaylist(); } + private async Task ConfigurePermissions() + { + if (_playlist is null) + { + _isCreator = false; + _canAdd = false; + _canRemove = false; + _canPlay = false; + } + else + { + _isCreator = _playlist.CreatorUserId.ToString() == _currentUserId; + + _canAdd = _isCreator + || _playlist.AddPermission == EditPermission.Everyone + || (_playlist.AddPermission == EditPermission.AuthorizedOnly && _isAuthenticated); + + _canRemove = _isCreator + || _playlist.RemovePermission == EditPermission.Everyone + || (_playlist.RemovePermission == EditPermission.AuthorizedOnly && _isAuthenticated); + + _canPlay = _isCreator + || _playlist.PlayPermission == ViewPermission.Everyone + || (_playlist.PlayPermission == ViewPermission.AuthorizedOnly && _isAuthenticated); + + if (_isCreator && _isAuthenticated) + { + _editPermissions = new UpdatePermissionsDto + { + ViewPermission = _playlist.ViewPermission, + AddPermission = _playlist.AddPermission, + RemovePermission = _playlist.RemovePermission, + PlayPermission = _playlist.PlayPermission, + }; + } + + } + } + private async Task LoadPlaylist() { try @@ -219,25 +273,8 @@ if (response?.Success == true) { _playlist = response.Data; - _isCreator = _playlist.CreatorUserId.ToString() == _currentUserId; - _canAdd = _isCreator - || _playlist.AddPermission == EditPermission.Everyone - || (_playlist.AddPermission == EditPermission.AuthorizedOnly && _isAuthenticated); - - _canRemove = _isCreator - || _playlist.RemovePermission == EditPermission.Everyone - || (_playlist.RemovePermission == EditPermission.AuthorizedOnly && _isAuthenticated); - - if (_isCreator && _isAuthenticated) - { - _editPermissions = new UpdatePermissionsDto - { - ViewPermission = _playlist.ViewPermission, - AddPermission = _playlist.AddPermission, - RemovePermission = _playlist.RemovePermission - }; - } + await ConfigurePermissions(); await LoadTracks(); } @@ -371,11 +408,8 @@ if (result?.Success == true) { _playlist = result.Data; - Snackbar.Add("Права доступа обновлены", Severity.Success); - _canAdd = _isCreator || _playlist.AddPermission == EditPermission.Everyone || - (_playlist.AddPermission == EditPermission.AuthorizedOnly && _isAuthenticated); - _canRemove = _isCreator || _playlist.RemovePermission == EditPermission.Everyone || - (_playlist.RemovePermission == EditPermission.AuthorizedOnly && _isAuthenticated); + + await ConfigurePermissions(); } else { diff --git a/PlaylistShared.Pwa/wwwroot/index.html b/PlaylistShared.Pwa/wwwroot/index.html index 4225d8a..73bbdd0 100644 --- a/PlaylistShared.Pwa/wwwroot/index.html +++ b/PlaylistShared.Pwa/wwwroot/index.html @@ -16,74 +16,6 @@ - diff --git a/PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js b/PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js index 8d373c7..9202e24 100644 --- a/PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js +++ b/PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js @@ -10,9 +10,10 @@ return isNaN(num) ? 0 : num; }; - const loadAndPlay = (src, token) => { + const loadAndPlay = (src, token, sharedPlaylistId) => { const url = new URL(src, window.location.href); if (token) url.searchParams.set('access_token', token); + if (sharedPlaylistId) url.searchParams.set('shared_id', sharedPlaylistId); audio.src = url.toString(); audio.load(); durationReady = false; diff --git a/PlaylistShared.Shared/Playlist/SharePlaylistDto.cs b/PlaylistShared.Shared/Playlist/SharePlaylistDto.cs index 7fd80af..8f620c2 100644 --- a/PlaylistShared.Shared/Playlist/SharePlaylistDto.cs +++ b/PlaylistShared.Shared/Playlist/SharePlaylistDto.cs @@ -38,6 +38,10 @@ public class SharePlaylistDto [JsonPropertyName("viewPermission")] public ViewPermission ViewPermission { get; set; } + /// Права на воспроизведение. + [JsonPropertyName("playPermission")] + public ViewPermission PlayPermission { get; set; } + /// Права на добавление треков. [JsonPropertyName("addPermission")] public EditPermission AddPermission { get; set; } diff --git a/PlaylistShared.Shared/Shared/SharedPlaylistDto.cs b/PlaylistShared.Shared/Shared/SharedPlaylistDto.cs index 5c510ca..3dc024e 100644 --- a/PlaylistShared.Shared/Shared/SharedPlaylistDto.cs +++ b/PlaylistShared.Shared/Shared/SharedPlaylistDto.cs @@ -55,6 +55,10 @@ public class SharedPlaylistDto [JsonPropertyName("viewPermission")] public ViewPermission ViewPermission { get; set; } + /// Права на воспроизведение. + [JsonPropertyName("playPermission")] + public ViewPermission PlayPermission { get; set; } + /// Права на добавление треков. [JsonPropertyName("addPermission")] public EditPermission AddPermission { get; set; } diff --git a/PlaylistShared.Shared/Shared/UpdatePermissionsDto.cs b/PlaylistShared.Shared/Shared/UpdatePermissionsDto.cs index 96aa2ea..281ee82 100644 --- a/PlaylistShared.Shared/Shared/UpdatePermissionsDto.cs +++ b/PlaylistShared.Shared/Shared/UpdatePermissionsDto.cs @@ -6,15 +6,19 @@ namespace PlaylistShared.Shared.Shared; /// Запрос на обновление прав доступа шеринг-плейлиста. public class UpdatePermissionsDto { - /// Новые права на просмотр. + /// Права на просмотр. [JsonPropertyName("viewPermission")] public ViewPermission ViewPermission { get; set; } - /// Новые права на добавление треков. + /// Права на воспроизведение треков. + [JsonPropertyName("playPermission")] + public ViewPermission PlayPermission { get; set; } + + /// Права на добавление треков. [JsonPropertyName("addPermission")] public EditPermission AddPermission { get; set; } - /// Новые права на удаление треков. + /// Права на удаление треков. [JsonPropertyName("removePermission")] public EditPermission RemovePermission { get; set; } } \ No newline at end of file