From dcb51448aaa70b780c4962dbcbdbb4e96077f229 Mon Sep 17 00:00:00 2001 From: KD Date: Tue, 11 Mar 2025 21:45:32 -0400 Subject: [PATCH] tsk-76: Added model and context for AccessLevel (#108) * tsk-76: Added model and context for AccessLevel * Formatted code * AccessLevel migrations are working * minor formatting * tsk-#76: Saving AccessLevel when uploading song * tsk-#76: Added endpoints * tsk-#76: Warning fix * tsk-#76: Adding more controller code * Functionality to modify access levels is operational * Formatting * tsk-#76: Added functionality * tsk-#76: Formatting code * tsk-#76: Adding more code * tsk-#76: Formatting --- Icarus/Controllers/Managers/SongManager.cs | 30 ++-- Icarus/Controllers/Managers/TokenManager.cs | 38 +++-- .../Controllers/v1/AccessLevelController.cs | 131 ++++++++++++++++++ .../v1/SongCompressedDataControllers.cs | 14 ++ Icarus/Controllers/v1/SongController.cs | 14 ++ Icarus/Controllers/v1/SongDataController.cs | 38 ++++- Icarus/Controllers/v1/SongStreamController.cs | 23 ++- .../Database/Contexts/AccessLevelContext.cs | 36 +++++ Icarus/Program.cs | 1 + Models/AccessLevel.cs | 51 +++++++ Scripts/Migrations/Linux/AddUpdate.sh | 4 + 11 files changed, 355 insertions(+), 25 deletions(-) create mode 100644 Icarus/Controllers/v1/AccessLevelController.cs create mode 100644 Icarus/Database/Contexts/AccessLevelContext.cs create mode 100644 Models/AccessLevel.cs diff --git a/Icarus/Controllers/Managers/SongManager.cs b/Icarus/Controllers/Managers/SongManager.cs index 0f25270..6e0dcd7 100644 --- a/Icarus/Controllers/Managers/SongManager.cs +++ b/Icarus/Controllers/Managers/SongManager.cs @@ -157,6 +157,10 @@ public class SongManager : BaseManager DeleteSongFromDatabase(song); coverMgr.DeleteCoverArtFromDatabase(coverArt); + var accessLevelContext = new AccessLevelContext(_config!.GetConnectionString("DefaultConnection")!); + var accessLevel = accessLevelContext.AccessLevels!.FirstOrDefault(al => al.SongId == song.Id); + accessLevelContext.AccessLevels!.Remove(accessLevel!); + accessLevelContext.SaveChanges(); } catch (Exception ex) { @@ -165,7 +169,7 @@ public class SongManager : BaseManager } } - + public Song SaveFlacSongToFileSystem(IFormFile songFile, IFormFile coverArtData, Song song) { // Save temp song (Should already be saved to the filesystem by the time it gets to this method) @@ -345,10 +349,10 @@ public class SongManager : BaseManager { _logger.Info("Starting process to save the song to the database"); - var albumMgr = new AlbumManager(_config!); - var artistMgr = new ArtistManager(_config!); - var genreMgr = new GenreManager(_config!); - var coverMgr = new CoverArtManager(_config!); + var albumMgr = new AlbumManager(this._config!); + var artistMgr = new ArtistManager(this._config!); + var genreMgr = new GenreManager(this._config!); + var coverMgr = new CoverArtManager(this._config!); albumMgr.SaveAlbumToDatabase(ref song); artistMgr.SaveArtistToDatabase(ref song); genreMgr.SaveGenreToDatabase(ref song); @@ -356,10 +360,20 @@ public class SongManager : BaseManager var info = "Saving Song to DB"; _logger.Info(info); - _songContext!.Add(song); - _songContext!.SaveChanges(); + this._songContext!.Add(song); + this._songContext!.SaveChanges(); - coverMgr.SaveCoverArtToDatabase(ref song, ref cover!); + if (cover != null) + { + coverMgr.SaveCoverArtToDatabase(ref song, ref cover!); + } + + var accessLevel = Icarus.Models.AccessLevel.DefaultLevel(); + accessLevel.SongId = song.Id; + + var accessLevelContext = new AccessLevelContext(this._connectionString!); + accessLevelContext.Add(accessLevel); + accessLevelContext.SaveChanges(); } diff --git a/Icarus/Controllers/Managers/TokenManager.cs b/Icarus/Controllers/Managers/TokenManager.cs index dfa1f2e..bbb79e7 100644 --- a/Icarus/Controllers/Managers/TokenManager.cs +++ b/Icarus/Controllers/Managers/TokenManager.cs @@ -121,27 +121,49 @@ public class TokenManager : BaseManager }; } - public int RetrieveUserIdFromToken(string token) + public bool CanAccessSong(string token, Song song, AccessLevel accessLevel) { - if (this.ContainsBearer(token)) + if (accessLevel!.Level!.Equals(Models.AccessLevel.DefaultLevel().Level)) { - token = this.StripBearer(token); + return true; + } + var tokenUserId = this.RetrieveUserIdFromToken(token); + if (tokenUserId == null) + { + return false; } + return tokenUserId.Value == song.UserId; + } + + public string? GetBearerToken(HttpContext context) + { + string authorizationHeader = context.Request.Headers["Authorization"]!; + + if (!authorizationHeader.IsNullOrEmpty() && authorizationHeader.StartsWith("Bearer", StringComparison.OrdinalIgnoreCase)) + { + string token = authorizationHeader.Substring("Bearer ".Length).Trim(); + return token; + } + + return null; + } + + public int? RetrieveUserIdFromToken(string token) + { + var parsedToken = this.ContainsBearer(token) ? this.StripBearer(token) : token; var tokenHandler = new JwtSecurityTokenHandler(); - var readTok = tokenHandler.ReadJwtToken(token); - var userId = -1; + var readTok = tokenHandler.ReadJwtToken(parsedToken); foreach (var item in readTok.Payload) { if (item.Key == "user_id") { - userId = Convert.ToInt32(item.Value); - break; + return Convert.ToInt32(item.Value); } } - return userId; + return null; } private string StripBearer(string token) diff --git a/Icarus/Controllers/v1/AccessLevelController.cs b/Icarus/Controllers/v1/AccessLevelController.cs new file mode 100644 index 0000000..1d74488 --- /dev/null +++ b/Icarus/Controllers/v1/AccessLevelController.cs @@ -0,0 +1,131 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Icarus.Controllers.V1; + +[Route("api/v1/accesslevel")] +[ApiController] +[Authorize] +public class AccessLevelController : BaseController +{ + #region Fields + private readonly ILogger? _logger; + private string? _connectionString; + #endregion + + #region Properties + #endregion + + #region Constructors + public AccessLevelController(ILogger logger, IConfiguration config) + { + this._logger = logger; + this._config = config; + this._connectionString = this._config.GetConnectionString("DefaultConnection"); + } + #endregion + + #region HTTP Routes + [HttpGet] + public IActionResult GetAccessLevels(int? id, int? songId) + { + var accLevel = new Models.AccessLevel { Id = 0 }; + var accessLevelContext = new Database.Contexts.AccessLevelContext(_connectionString!); + + if (id != null) + { + accLevel = accessLevelContext.AccessLevels!.FirstOrDefault(al => al.Id == id); + } + else if (songId != null) + { + accLevel = accessLevelContext.GetAccessLevel(songId.Value); + } + + var response = new GetAccessLevelsResponse { Data = new List() }; + + if (accLevel?.Id > 0) + { + response.Subject = "Successful"; + response.Data.Add(accLevel); + return Ok(response); + } + else + { + response.Subject = "Failure"; + return BadRequest(response); + } + } + + [HttpPatch("{id}")] + public IActionResult UpdateAccessLevel(int id, [FromBody] Models.AccessLevel accessLevel) + { + var response = new UpdateAccessLevelResponse { Data = new List() }; + var targetLevel = accessLevel.Level; + if (targetLevel == null) + { + response.Subject = "No level provided"; + return BadRequest(response); + } + else + { + if (!Models.AccessLevel.IsAccessLevelValid(targetLevel)) + { + response.Subject = "Invalid level"; + return BadRequest(response); + } + } + + var accessLevelContext = new Database.Contexts.AccessLevelContext(this._connectionString!); + + var fetchedAccLevel = accessLevelContext.AccessLevels!.FirstOrDefault(al => al.Id == id); + + if (fetchedAccLevel == null) + { + response.Subject = "Nothing found"; + return NotFound(response); + } + + var fetchedLevel = fetchedAccLevel!.Level; + + if (fetchedLevel!.Equals(targetLevel)) + { + // No change + response.Subject = "No change"; + response.Data.Add(fetchedAccLevel); + return Ok(response); + } + else + { + fetchedAccLevel.Level = targetLevel; + response.Subject = "Successful"; + accessLevelContext.Update(fetchedAccLevel); + accessLevelContext.SaveChanges(); + + response.Data.Add(fetchedAccLevel); + return Ok(response); + } + } + #endregion +} + +#region Responses +public class GetAccessLevelsResponse +{ + #region Properties + [Newtonsoft.Json.JsonProperty("subject")] + public string? Subject { get; set; } + [Newtonsoft.Json.JsonProperty("data")] + public List? Data { get; set; } + #endregion +} + +public class UpdateAccessLevelResponse +{ + #region Properties + [Newtonsoft.Json.JsonProperty("subject")] + public string? Subject { get; set; } + [Newtonsoft.Json.JsonProperty("data")] + public List? Data { get; set; } + #endregion +} +#endregion diff --git a/Icarus/Controllers/v1/SongCompressedDataControllers.cs b/Icarus/Controllers/v1/SongCompressedDataControllers.cs index 57dd630..d82864b 100644 --- a/Icarus/Controllers/v1/SongCompressedDataControllers.cs +++ b/Icarus/Controllers/v1/SongCompressedDataControllers.cs @@ -47,6 +47,20 @@ public class SongCompressedDataController : BaseController Console.WriteLine("Starting process of retrieving comrpessed song"); var sng = context.RetrieveRecord(new Song { Id = id }); + + var tokenManager = new TokenManager(this._config!); + var accLvlContext = new AccessLevelContext(this._connectionString!); + var accessLevel = accLvlContext.GetAccessLevel(sng.Id); + var token = tokenManager.GetBearerToken(HttpContext); + if (token == null || accessLevel == null) + { + return BadRequest(); + } + + if (!tokenManager.CanAccessSong(token, sng, accessLevel)) + { + return BadRequest(); + } SongData song = await cmp.RetrieveCompressedSong(sng); var filename = DirectoryManager.GenerateDownloadFilename(10, Constants.FileExtensions.ZIP_EXTENSION, sng.Title!, randomizeFilename); diff --git a/Icarus/Controllers/v1/SongController.cs b/Icarus/Controllers/v1/SongController.cs index 6011260..f59a0c9 100644 --- a/Icarus/Controllers/v1/SongController.cs +++ b/Icarus/Controllers/v1/SongController.cs @@ -86,6 +86,20 @@ public class SongController : BaseController }); } + var tokenManager = new TokenManager(this._config!); + var accLvlContext = new AccessLevelContext(this._connectionString!); + var accessLevel = accLvlContext.GetAccessLevel(song.Id); + var token = tokenManager.GetBearerToken(HttpContext); + if (token == null || accessLevel == null) + { + return BadRequest(); + } + + if (!tokenManager.CanAccessSong(token, song, accessLevel)) + { + return BadRequest(); + } + var songRes = _songMgr.UpdateSong(song); return Ok(songRes); diff --git a/Icarus/Controllers/v1/SongDataController.cs b/Icarus/Controllers/v1/SongDataController.cs index 7626853..4cbd4b4 100644 --- a/Icarus/Controllers/v1/SongDataController.cs +++ b/Icarus/Controllers/v1/SongDataController.cs @@ -38,11 +38,25 @@ public class SongDataController : BaseController #endregion + [HttpGet("download/{id}")] public IActionResult Download(int id, [FromQuery] bool? randomizeFilename) { - var songContext = new SongContext(_connectionString!); + var tokenManager = new TokenManager(this._config!); + var songContext = new SongContext(this._connectionString!); + var accLvlContext = new AccessLevelContext(this._connectionString!); var songMetaData = songContext.RetrieveRecord(new Song { Id = id }); + var accessLevel = accLvlContext.GetAccessLevel(songMetaData.Id); + var token = tokenManager.GetBearerToken(HttpContext); + if (token == null || accessLevel == null) + { + return BadRequest(); + } + + if (!tokenManager.CanAccessSong(token, songMetaData, accessLevel)) + { + return BadRequest(); + } var song = _songMgr!.RetrieveSong(songMetaData).Result; string filename; @@ -66,6 +80,7 @@ public class SongDataController : BaseController return File(song.Data!, "application/x-msdownload", filename); } + // NOTE: No longer being used // Assumes that the song already has metadata such as // Title // Artist @@ -116,9 +131,9 @@ public class SongDataController : BaseController var accessToken = Request.Headers["Authorization"]; var userId = tokMgr.RetrieveUserIdFromToken(accessToken!); - if (userId != -1) + if (userId != null) { - song!.UserId = userId; + song!.UserId = userId.Value; } _logger!.LogInformation($"Song title: {song!.Title}"); @@ -160,11 +175,21 @@ public class SongDataController : BaseController public IActionResult DeleteSong(int id) { var songContext = new SongContext(_connectionString!); + var songMetaData = songContext.RetrieveRecord(new Song { Id = id }); - var songMetaData = new Song { Id = id }; - Console.WriteLine($"Id {songMetaData.Id}"); + var tokenManager = new TokenManager(this._config!); + var accLvlContext = new AccessLevelContext(this._connectionString!); + var accessLevel = accLvlContext.GetAccessLevel(songMetaData.Id); + var token = tokenManager.GetBearerToken(HttpContext); + if (token == null || accessLevel == null) + { + return BadRequest(); + } - songMetaData = songContext.RetrieveRecord(songMetaData); + if (!tokenManager.CanAccessSong(token, songMetaData, accessLevel)) + { + return BadRequest(); + } if (string.IsNullOrEmpty(songMetaData.Title)) { @@ -179,7 +204,6 @@ public class SongDataController : BaseController return Ok(songMetaData); } - } public class UploadSongWithDataForm diff --git a/Icarus/Controllers/v1/SongStreamController.cs b/Icarus/Controllers/v1/SongStreamController.cs index 7a347bb..f38a966 100644 --- a/Icarus/Controllers/v1/SongStreamController.cs +++ b/Icarus/Controllers/v1/SongStreamController.cs @@ -11,6 +11,7 @@ namespace Icarus.Controllers.V1; public class SongStreamController : BaseController { #region Fields + private string? _connectionString; private ILogger? _logger; #endregion @@ -22,8 +23,9 @@ public class SongStreamController : BaseController #region Constructor public SongStreamController(ILogger logger, IConfiguration config) { - _logger = logger; - _config = config; + this._logger = logger; + this._config = config; + this._connectionString = this._config.GetConnectionString("DefaultConnection"); } #endregion @@ -35,6 +37,23 @@ public class SongStreamController : BaseController var context = new SongContext(_config!.GetConnectionString("DefaultConnection")!); var song = context.Songs!.FirstOrDefault(sng => sng.Id == id); + if (song == null) + { + return BadRequest(); + } + var tokenManager = new Managers.TokenManager(this._config!); + var accLvlContext = new AccessLevelContext(this._connectionString!); + var accessLevel = accLvlContext.GetAccessLevel(song.Id); + var token = tokenManager.GetBearerToken(HttpContext); + if (token == null || accessLevel == null) + { + return BadRequest(); + } + + if (!tokenManager.CanAccessSong(token, song, accessLevel)) + { + return BadRequest(); + } var stream = new FileStream(song!.SongPath(), FileMode.Open, FileAccess.Read); stream.Position = 0; diff --git a/Icarus/Database/Contexts/AccessLevelContext.cs b/Icarus/Database/Contexts/AccessLevelContext.cs new file mode 100644 index 0000000..ac8a5ea --- /dev/null +++ b/Icarus/Database/Contexts/AccessLevelContext.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore; + +namespace Icarus.Database.Contexts; + +public class AccessLevelContext : DbContext +{ + #region Properties + public DbSet? AccessLevels { get; set; } + #endregion + + #region Constructors + public AccessLevelContext(DbContextOptions options) : base(options) { } + public AccessLevelContext(string connString) : base(new DbContextOptionsBuilder() + .UseMySQL(connString).Options) + { + if (this.AccessLevels == null) + { + } + } + #endregion + + #region Methods + public Models.AccessLevel? GetAccessLevel(int songId) + { + var accessLevel = this.AccessLevels!.FirstOrDefault(acc => acc.SongId == songId); + return accessLevel; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable("AccessLevel"); + + modelBuilder.Entity().Property(m => m.Level).IsRequired(true); + } + #endregion +} diff --git a/Icarus/Program.cs b/Icarus/Program.cs index 8a8377f..a9f313a 100644 --- a/Icarus/Program.cs +++ b/Icarus/Program.cs @@ -76,6 +76,7 @@ builder.Services.AddDbContext(options => options.UseMySQL(connStr builder.Services.AddDbContext(options => options.UseMySQL(connString!)); builder.Services.AddDbContext(options => options.UseMySQL(connString!)); builder.Services.AddDbContext(options => options.UseMySQL(connString!)); +builder.Services.AddDbContext(options => options.UseMySQL(connString!)); builder.Services.AddControllers() .AddNewtonsoftJson(); diff --git a/Models/AccessLevel.cs b/Models/AccessLevel.cs new file mode 100644 index 0000000..d983b31 --- /dev/null +++ b/Models/AccessLevel.cs @@ -0,0 +1,51 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Icarus.Models; + +public class AccessLevel +{ + #region Properties + [Newtonsoft.Json.JsonProperty("id")] + public int Id { get; set; } + [Newtonsoft.Json.JsonProperty("level")] + public string? Level { get; set; } + [Newtonsoft.Json.JsonProperty("song_id")] + public int SongId { get; set; } + #endregion + + #region Methods + public static AccessLevel DefaultLevel() + { + return new AccessLevel + { + Level = "Public" + }; + } + + public static AccessLevel PrivateLevel() + { + return new AccessLevel + { + Level = "Private" + }; + } + + public static bool IsAccessLevelValid(string level) + { + if (level.Equals(DefaultLevel().Level)) + { + return true; + } + else if (level.Equals(PrivateLevel().Level)) + { + return true; + } + else + { + return false; + } + } + #endregion +} + + diff --git a/Scripts/Migrations/Linux/AddUpdate.sh b/Scripts/Migrations/Linux/AddUpdate.sh index ed4ba7b..6c8f01a 100755 --- a/Scripts/Migrations/Linux/AddUpdate.sh +++ b/Scripts/Migrations/Linux/AddUpdate.sh @@ -12,6 +12,8 @@ echo "Adding Cover art migration" dotnet dotnet-ef migrations add CoverArt --context CoverArtContext echo "Adding Song migration" dotnet dotnet-ef migrations add Song --context SongContext +echo "Adding AccessLevel migration" +dotnet dotnet-ef migrations add AccessLevel --context AccessLevelContext echo "Updating migrations.." echo "Updating User migration" @@ -26,4 +28,6 @@ echo "Updating Cover art migration" dotnet dotnet-ef database update --context CoverArtContext echo "Updating Song migration" dotnet dotnet-ef database update --context SongContext +echo "Updating AccessLevel migration" +dotnet dotnet-ef database update --context AccessLevelContext