Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 80628292e2 | |||
| 57f4ec1261 | |||
| c191d7ee7a | |||
| 73bfff3940 | |||
| 38c056cc99 | |||
| 640971dea8 | |||
| 664b0d520b | |||
| 11fe1f29e1 | |||
| 05b5de0939 | |||
| d5944c470b | |||
| 00ef8d0242 | |||
| 77c6ee00ea |
@@ -4,10 +4,14 @@ using System.Linq;
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
using JWT;
|
using JWT;
|
||||||
using JWT.Serializers;
|
using JWT.Serializers;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Org.BouncyCastle.Crypto;
|
using Org.BouncyCastle.Crypto;
|
||||||
using Org.BouncyCastle.Crypto.Parameters;
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
@@ -113,6 +117,40 @@ namespace Icarus.Controllers.Managers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LoginResult LoginSymmetric(User user)
|
||||||
|
{
|
||||||
|
var tokenResult = new TokenTierOne();
|
||||||
|
tokenResult.TokenType = "Jwt";
|
||||||
|
|
||||||
|
var payload = Payload();
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JWT:Secret"]));
|
||||||
|
var signIn = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
_config["JWT:Issuer"],
|
||||||
|
_config["JWT:Audience"],
|
||||||
|
payload,
|
||||||
|
expires: DateTime.UtcNow.AddMinutes(30),
|
||||||
|
signingCredentials: signIn);
|
||||||
|
|
||||||
|
tokenResult.AccessToken = new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
|
||||||
|
var expClaim = payload.FirstOrDefault(cl =>
|
||||||
|
{
|
||||||
|
return cl.Type.Equals("exp");
|
||||||
|
});
|
||||||
|
|
||||||
|
var expiredDate = DateTime.Parse(expClaim.Value);
|
||||||
|
var exp = Math.Floor((expiredDate - DateTime.UnixEpoch).TotalSeconds);
|
||||||
|
tokenResult.Expiration = Convert.ToInt32(exp);
|
||||||
|
|
||||||
|
return new LoginResult
|
||||||
|
{
|
||||||
|
UserID = user.UserID, Username = user.Username, Token = tokenResult.AccessToken,
|
||||||
|
TokenType = tokenResult.TokenType, Expiration = tokenResult.Expiration,
|
||||||
|
Message = "Successfully retrieved token"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsTokenValid(string scope, string accessToken)
|
public bool IsTokenValid(string scope, string accessToken)
|
||||||
{
|
{
|
||||||
var result = false;
|
var result = false;
|
||||||
@@ -201,23 +239,25 @@ namespace Icarus.Controllers.Managers
|
|||||||
|
|
||||||
private List<Claim> Payload()
|
private List<Claim> Payload()
|
||||||
{
|
{
|
||||||
const int expLimit = 24;
|
var expLimit = 30;
|
||||||
var currentDate = DateTime.Now;
|
var currentDate = DateTime.Now;
|
||||||
var expiredDate = currentDate.AddHours(expLimit);
|
var expiredDate = currentDate.AddMinutes(expLimit);
|
||||||
var issued = Math.Floor((currentDate - DateTime.UnixEpoch).TotalSeconds);
|
var issued = Math.Floor((currentDate - DateTime.UnixEpoch).TotalSeconds);
|
||||||
var expires = Math.Floor((expiredDate - DateTime.UnixEpoch).TotalSeconds);
|
var expires = Math.Floor((expiredDate - DateTime.UnixEpoch).TotalSeconds);
|
||||||
var issuer = "https://soaricarus.auth0.com";
|
var issuer = "https://soaricarus.auth0.com";
|
||||||
issuer = "http://localhost:5002";
|
issuer = "http://localhost:5002";
|
||||||
var audience = "https://icarus/api";
|
var audience = "https://icarus/api";
|
||||||
audience = "http://localhost:5002";
|
audience = "http://localhost:5002";
|
||||||
|
var subject = _config["JWT:Subject"];
|
||||||
|
|
||||||
var claim = new List<System.Security.Claims.Claim>()
|
var claim = new List<System.Security.Claims.Claim>()
|
||||||
{
|
{
|
||||||
new System.Security.Claims.Claim("scope", AllScopes(), "string"),
|
new System.Security.Claims.Claim("scope", AllScopes(), "string"),
|
||||||
new System.Security.Claims.Claim("exp", $"{expires}", "integer"),
|
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Exp, expiredDate.ToString()),
|
||||||
new System.Security.Claims.Claim("aud", $"{audience}", "string"),
|
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Aud, audience),
|
||||||
new System.Security.Claims.Claim("iss", $"{issuer}", "string"),
|
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Iss, issuer),
|
||||||
new System.Security.Claims.Claim("iat", $"{issued}", "integer")
|
new Claim(JwtRegisteredClaimNames.Sub, subject),
|
||||||
|
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Iat, currentDate.ToString())
|
||||||
};
|
};
|
||||||
|
|
||||||
return claim;
|
return claim;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[Route("api/v1/album")]
|
[Route("api/v1/album")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
public class AlbumController : BaseController
|
public class AlbumController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
@@ -38,13 +39,8 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
#region HTTP Routes
|
#region HTTP Routes
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult Get()
|
public IActionResult GetAlbums()
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("read:albums"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Album> albums = new List<Album>();
|
List<Album> albums = new List<Album>();
|
||||||
|
|
||||||
var albumContext = new AlbumContext(_connectionString);
|
var albumContext = new AlbumContext(_connectionString);
|
||||||
@@ -58,13 +54,8 @@ namespace Icarus.Controllers.V1
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public IActionResult Get(int id)
|
public IActionResult GetAlbum(int id)
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("read:albums"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
Album album = new Album
|
Album album = new Album
|
||||||
{
|
{
|
||||||
AlbumID = id
|
AlbumID = id
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[Route("api/v1/artist")]
|
[Route("api/v1/artist")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
public class ArtistController : BaseController
|
public class ArtistController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
@@ -37,13 +38,8 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
#region HTTP Routes
|
#region HTTP Routes
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult Get()
|
public IActionResult GetArtists()
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("read:artists"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
var artistContext = new ArtistContext(_connectionString);
|
var artistContext = new ArtistContext(_connectionString);
|
||||||
|
|
||||||
var artists = artistContext.Artists.ToList();
|
var artists = artistContext.Artists.ToList();
|
||||||
@@ -55,7 +51,7 @@ namespace Icarus.Controllers.V1
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public IActionResult Get(int id)
|
public IActionResult GetArtist(int id)
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("read:artists"))
|
if (!IsTokenValid("read:artists"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
|
|
||||||
#region Methods
|
#region Methods
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
protected string ParseBearerTokenFromHeader()
|
protected string ParseBearerTokenFromHeader()
|
||||||
{
|
{
|
||||||
var token = string.Empty;
|
var token = string.Empty;
|
||||||
@@ -37,6 +38,7 @@ namespace Icarus.Controllers.V1
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
protected bool IsTokenValid(string scope)
|
protected bool IsTokenValid(string scope)
|
||||||
{
|
{
|
||||||
var token = ParseBearerTokenFromHeader();
|
var token = ParseBearerTokenFromHeader();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[Route("api/v1/coverart")]
|
[Route("api/v1/coverart")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
public class CoverArtController : BaseController
|
public class CoverArtController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
@@ -34,13 +35,9 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
|
|
||||||
#region HTTP Routes
|
#region HTTP Routes
|
||||||
public IActionResult Get()
|
[HttpGet]
|
||||||
|
public IActionResult GetCoverArts()
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("read:songs"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
var coverArtContext = new CoverArtContext(_connectionString);
|
var coverArtContext = new CoverArtContext(_connectionString);
|
||||||
|
|
||||||
var coverArtRecords = coverArtContext.CoverArtImages.ToList();
|
var coverArtRecords = coverArtContext.CoverArtImages.ToList();
|
||||||
@@ -58,13 +55,8 @@ namespace Icarus.Controllers.V1
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public async Task<IActionResult> Get(int id)
|
public IActionResult GetCoverArt(int id)
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("download:cover_art"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
var coverArt = new CoverArt { CoverArtID = id };
|
var coverArt = new CoverArt { CoverArtID = id };
|
||||||
|
|
||||||
var coverArtContext = new CoverArtContext(_connectionString);
|
var coverArtContext = new CoverArtContext(_connectionString);
|
||||||
@@ -74,7 +66,7 @@ namespace Icarus.Controllers.V1
|
|||||||
if (coverArt != null)
|
if (coverArt != null)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Found cover art record");
|
_logger.LogInformation("Found cover art record");
|
||||||
var coverArtBytes = await System.IO.File.ReadAllBytesAsync(
|
var coverArtBytes = System.IO.File.ReadAllBytes(
|
||||||
coverArt.ImagePath);
|
coverArt.ImagePath);
|
||||||
|
|
||||||
return File(coverArtBytes, "application/x-msdownload",
|
return File(coverArtBytes, "application/x-msdownload",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[Route("api/v1/genre")]
|
[Route("api/v1/genre")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
public class GenreController : BaseController
|
public class GenreController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
@@ -38,13 +39,8 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
#region HTTP Routes
|
#region HTTP Routes
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult Get()
|
public IActionResult GetGenres()
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("read:genre"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
var genres = new List<Genre>();
|
var genres = new List<Genre>();
|
||||||
|
|
||||||
var genreStore = new GenreContext(_connectionString);
|
var genreStore = new GenreContext(_connectionString);
|
||||||
@@ -58,13 +54,8 @@ namespace Icarus.Controllers.V1
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public IActionResult Get(int id)
|
public IActionResult GetGenre(int id)
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("read:genre"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
var genre = new Genre
|
var genre = new Genre
|
||||||
{
|
{
|
||||||
GenreID = id
|
GenreID = id
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Configuration;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
@@ -41,7 +38,8 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
|
|
||||||
#region HTTP endpoints
|
#region HTTP endpoints
|
||||||
public IActionResult Post([FromBody] User user)
|
[HttpPost]
|
||||||
|
public IActionResult Login([FromBody] User user)
|
||||||
{
|
{
|
||||||
var context = new UserContext(_connectionString);
|
var context = new UserContext(_connectionString);
|
||||||
|
|
||||||
@@ -55,34 +53,44 @@ namespace Icarus.Controllers.V1
|
|||||||
Username = user.Username
|
Username = user.Username
|
||||||
};
|
};
|
||||||
|
|
||||||
if (context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username)) != null)
|
try
|
||||||
{
|
{
|
||||||
user = context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username));
|
if (context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username)) != null)
|
||||||
|
|
||||||
var validatePass = new PasswordEncryption();
|
|
||||||
var validated = validatePass.VerifyPassword(user, password);
|
|
||||||
if (!validated)
|
|
||||||
{
|
{
|
||||||
loginRes.Message = message;
|
user = context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username));
|
||||||
_logger.LogInformation(message);
|
|
||||||
|
var validatePass = new PasswordEncryption();
|
||||||
|
var validated = validatePass.VerifyPassword(user, password);
|
||||||
|
if (!validated)
|
||||||
|
{
|
||||||
|
loginRes.Message = message;
|
||||||
|
_logger.LogInformation(message);
|
||||||
|
|
||||||
|
return Ok(loginRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully validated user credentials");
|
||||||
|
|
||||||
|
TokenManager tk = new TokenManager(_config);
|
||||||
|
|
||||||
|
loginRes = tk.LoginSymmetric(user);
|
||||||
|
|
||||||
return Ok(loginRes);
|
return Ok(loginRes);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
loginRes.Message = message;
|
||||||
|
|
||||||
_logger.LogInformation("Successfully validated user credentials");
|
return NotFound(loginRes);
|
||||||
|
}
|
||||||
TokenManager tk = new TokenManager(_config);
|
|
||||||
|
|
||||||
loginRes = tk.LogIn(user);
|
|
||||||
|
|
||||||
return Ok(loginRes);
|
|
||||||
}
|
}
|
||||||
else
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
loginRes.Message = message;
|
_logger.LogError("An error occurred: {0}", ex.Message);
|
||||||
|
_logger.LogError("Inner Exception: {0}", ex.InnerException.Message);
|
||||||
return NotFound(loginRes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return NotFound(loginRes);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ namespace Icarus.Controllers.V1
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public IActionResult Post([FromBody] User user)
|
public IActionResult RegisterUser([FromBody] User user)
|
||||||
{
|
{
|
||||||
PasswordEncryption pe = new PasswordEncryption();
|
PasswordEncryption pe = new PasswordEncryption();
|
||||||
user.Password = pe.HashPassword(user);
|
user.Password = pe.HashPassword(user);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[Route("api/v1/song/compressed/data")]
|
[Route("api/v1/song/compressed/data")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
public class SongCompressedDataController : BaseController
|
public class SongCompressedDataController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
@@ -45,13 +46,8 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
#region API Routes
|
#region API Routes
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public async Task<IActionResult> Get(int id)
|
public async Task<IActionResult> DownloadCompressedSong(int id)
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("download:songs"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
var context = new SongContext(_connectionString);
|
var context = new SongContext(_connectionString);
|
||||||
|
|
||||||
SongCompression cmp = new SongCompression(_archiveDir);
|
SongCompression cmp = new SongCompression(_archiveDir);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[Route("api/v1/song")]
|
[Route("api/v1/song")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
public class SongController : BaseController
|
public class SongController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
@@ -47,13 +48,8 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult Get()
|
public IActionResult GetSongs()
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("read:song_details"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Song> songs = new List<Song>();
|
List<Song> songs = new List<Song>();
|
||||||
Console.WriteLine("Attemtping to retrieve songs");
|
Console.WriteLine("Attemtping to retrieve songs");
|
||||||
_logger.LogInformation("Attempting to retrieve songs");
|
_logger.LogInformation("Attempting to retrieve songs");
|
||||||
@@ -69,13 +65,8 @@ namespace Icarus.Controllers.V1
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public IActionResult Get(int id)
|
public IActionResult GetSong(int id)
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("read:song_details"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
var context = new SongContext(_connectionString);
|
var context = new SongContext(_connectionString);
|
||||||
|
|
||||||
Song song = new Song { SongID = id };
|
Song song = new Song { SongID = id };
|
||||||
@@ -90,13 +81,8 @@ namespace Icarus.Controllers.V1
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
[HttpPut("{id}")]
|
||||||
public IActionResult Put(int id, [FromBody] Song song)
|
public IActionResult UpdateSong(int id, [FromBody] Song song)
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("update:songs"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
var context = new SongContext(_connectionString);
|
var context = new SongContext(_connectionString);
|
||||||
|
|
||||||
song.SongID = id;
|
song.SongID = id;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[Route("api/v1/song/data")]
|
[Route("api/v1/song/data")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
public class SongDataController : BaseController
|
public class SongDataController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
@@ -47,18 +48,12 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
|
|
||||||
[HttpGet("download/{id}")]
|
[HttpGet("download/{id}")]
|
||||||
[Route("private-scoped")]
|
public IActionResult Download(int id)
|
||||||
public async Task<IActionResult> Get(int id)
|
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("download:songs"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
var songContext = new SongContext(_connectionString);
|
var songContext = new SongContext(_connectionString);
|
||||||
var songMetaData = songContext.RetrieveRecord(new Song { SongID = id});
|
var songMetaData = songContext.RetrieveRecord(new Song { SongID = id});
|
||||||
|
|
||||||
var song = await _songMgr.RetrieveSong(songMetaData);
|
var song = _songMgr.RetrieveSong(songMetaData).Result;
|
||||||
|
|
||||||
return File(song.Data, "application/x-msdownload", songMetaData.Filename);
|
return File(song.Data, "application/x-msdownload", songMetaData.Filename);
|
||||||
}
|
}
|
||||||
@@ -76,14 +71,8 @@ namespace Icarus.Controllers.V1
|
|||||||
// Cover art
|
// Cover art
|
||||||
//
|
//
|
||||||
[HttpPost("upload"), DisableRequestSizeLimit]
|
[HttpPost("upload"), DisableRequestSizeLimit]
|
||||||
[Route("private-scoped")]
|
public IActionResult Upload([FromForm(Name = "file")] List<IFormFile> songData)
|
||||||
public IActionResult Post([FromForm(Name = "file")] List<IFormFile> songData)
|
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("upload:songs"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Console.WriteLine("Uploading song...");
|
// Console.WriteLine("Uploading song...");
|
||||||
@@ -118,14 +107,8 @@ namespace Icarus.Controllers.V1
|
|||||||
// as well as the cover art
|
// as well as the cover art
|
||||||
//
|
//
|
||||||
[HttpPost("upload/with/data")]
|
[HttpPost("upload/with/data")]
|
||||||
[Route("private-scoped")]
|
public IActionResult UploadWithData([FromForm] UploadSongWithDataForm up)
|
||||||
public IActionResult Post ([FromForm] UploadSongWithDataForm up)
|
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("upload:songs"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (up.SongData.Length > 0 && up.CoverArtData.Length > 0 && !string.IsNullOrEmpty(up.SongFile))
|
if (up.SongData.Length > 0 && up.CoverArtData.Length > 0 && !string.IsNullOrEmpty(up.SongFile))
|
||||||
@@ -145,13 +128,8 @@ namespace Icarus.Controllers.V1
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("delete/{id}")]
|
[HttpDelete("delete/{id}")]
|
||||||
public IActionResult Delete(int id)
|
public IActionResult DeleteSong(int id)
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("delete:songs"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
var songContext = new SongContext(_connectionString);
|
var songContext = new SongContext(_connectionString);
|
||||||
|
|
||||||
var songMetaData = new Song{ SongID = id };
|
var songMetaData = new Song{ SongID = id };
|
||||||
@@ -175,15 +153,15 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UploadSongWithDataForm
|
public class UploadSongWithDataForm
|
||||||
{
|
{
|
||||||
[FromForm(Name = "file")]
|
[FromForm(Name = "file")]
|
||||||
public IFormFile SongData { get; set; }
|
public IFormFile SongData { get; set; }
|
||||||
// TODO: Think about making this optional and if it is not provided, use the stock cover art
|
// NOTE: Think about making this optional and if it is not provided, use the stock cover art
|
||||||
[FromForm(Name = "cover")]
|
[FromForm(Name = "cover")]
|
||||||
public IFormFile CoverArtData { get; set; }
|
public IFormFile CoverArtData { get; set; }
|
||||||
[FromForm(Name = "metadata")]
|
[FromForm(Name = "metadata")]
|
||||||
public string SongFile { get; set; }
|
public string SongFile { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[Route("api/v1/song/stream")]
|
[Route("api/v1/song/stream")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
public class SongStreamController : BaseController
|
public class SongStreamController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
@@ -44,13 +45,8 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
#region HTTP endpoints
|
#region HTTP endpoints
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public async Task<IActionResult> Get(int id)
|
public async Task<IActionResult> StreamSong(int id)
|
||||||
{
|
{
|
||||||
if (!IsTokenValid("stream:songs"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
var context = new SongContext(_config.GetConnectionString("DefaultConnection"));
|
var context = new SongContext(_config.GetConnectionString("DefaultConnection"));
|
||||||
|
|
||||||
var song = context.Songs.FirstOrDefault(sng => sng.SongID == id);
|
var song = context.Songs.FirstOrDefault(sng => sng.SongID == id);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
|
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
|
||||||
<PackageReference Include="RestSharp" Version="106.15.0" />
|
<PackageReference Include="RestSharp" Version="106.15.0" />
|
||||||
<PackageReference Include="SevenZip" Version="19.0.0" />
|
<PackageReference Include="SevenZip" Version="19.0.0" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.15.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.15.0" />
|
||||||
<PackageReference Include="taglib" Version="2.1.0" />
|
<PackageReference Include="taglib" Version="2.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"IIS Express": {
|
"IIS Express": {
|
||||||
"commandName": "IISExpress",
|
"commandName": "IISExpress",
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"launchUrl": "api/values",
|
"launchUrl": "swagger",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
"Icarus": {
|
"Icarus": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"launchUrl": "api/values",
|
"dotnetRuneMessage": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
"applicationUrl": "http://localhost:5002",
|
"applicationUrl": "http://localhost:5002",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
|||||||
@@ -37,32 +37,29 @@ One can interface with Icarus the music server either by:
|
|||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
There are several things that need to be completed to properly setup and secure the API.
|
There are several things that need to be completed to properly setup and secure the API.
|
||||||
1. Creating RSA keys
|
This API uses OpenAPI Specification 3.0. After configuring the API, launch the software
|
||||||
|
and navigate your browser to https://localhost:5001/swagger to view the endpoints.
|
||||||
|
|
||||||
|
1. JWT Information
|
||||||
2. API filesystem paths
|
2. API filesystem paths
|
||||||
3. Database connection string
|
3. Database connection string
|
||||||
4. Migrations
|
4. Migrations
|
||||||
|
|
||||||
|
|
||||||
### Creating RSA keys
|
### JWT Information
|
||||||
|
|
||||||
1. Create private key
|
Configure JWT information. Notably the Secret
|
||||||
```
|
|
||||||
openssl genrsa -out private.pem 2048
|
|
||||||
```
|
|
||||||
2. Create public key
|
|
||||||
```
|
|
||||||
openssl rsa -in private -pubout -out public.pem
|
|
||||||
```
|
|
||||||
|
|
||||||
Configure the key paths in the config files
|
|
||||||
|
|
||||||
```Json
|
```Json
|
||||||
"RSAKeys": {
|
"JWT": {
|
||||||
"PrivateKeyPath": "",
|
"Issuer": "IcarusAPI",
|
||||||
"PublicKeyPath": ""
|
"Audience": "IcarusAPIClient",
|
||||||
}
|
"Secret": "Manaiswhatyouthinkitis",
|
||||||
|
"Subject": "Authorization"
|
||||||
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
Replace [domain] with the domain name that represent's your domain. Replace [identifier] with the identifer root name in the appsettings environment file. Not the friendly name but the root name of the identifier, omitting the http protocol and the *api* path.
|
Replace [domain] with the domain name that represent's your domain. Replace [identifier] with the identifer root name in the appsettings environment file. Not the friendly name but the root name of the identifier, omitting the http protocol and the *api* path.
|
||||||
|
|
||||||
```Json
|
```Json
|
||||||
@@ -72,6 +69,9 @@ Replace [domain] with the domain name that represent's your domain. Replace [ide
|
|||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note**: The Auth0 section is likely to be changed or removed in future releases.
|
||||||
|
|
||||||
|
|
||||||
### API filesystem paths
|
### API filesystem paths
|
||||||
|
|
||||||
For the purposes of properly uploading, downloading, updating, deleting, and streaming songs the API filesystem paths must be configured. What is meant by this is that the `RootMusicPath` directory where all music will be stored must exist as well as the `ArchivePath` and `TemporaryMusicPath` paths. An example on a Linux system:
|
For the purposes of properly uploading, downloading, updating, deleting, and streaming songs the API filesystem paths must be configured. What is meant by this is that the `RootMusicPath` directory where all music will be stored must exist as well as the `ArchivePath` and `TemporaryMusicPath` paths. An example on a Linux system:
|
||||||
@@ -140,11 +140,6 @@ From this point the database has been successfully configured. Metadata and song
|
|||||||
|
|
||||||
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the code of conduct, and the process for submitting pull requests to the project.
|
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the code of conduct, and the process for submitting pull requests to the project.
|
||||||
|
|
||||||
## Authors
|
|
||||||
|
|
||||||
* [kdeng00](https://github.com/kdeng00)
|
|
||||||
|
|
||||||
See also the list of [contributors](https://github.com/amazing-username/Icarus/graphs/contributors) who participated in this project.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+55
-39
@@ -1,13 +1,18 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NLog.Web;
|
using NLog.Web;
|
||||||
@@ -17,35 +22,6 @@ using Icarus.Database.Contexts;
|
|||||||
|
|
||||||
namespace Icarus
|
namespace Icarus
|
||||||
{
|
{
|
||||||
public static class ServiceStartup
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddAsymmetricAuthentication(this IServiceCollection services, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
var issuerSigningCertificate = new Icarus.Certs.SigningIssuerCertificate();
|
|
||||||
RsaSecurityKey issuerSigningKey = issuerSigningCertificate.GetIssuerSigningKey(configuration["RSAKeys:PublicKeyPath"]);
|
|
||||||
|
|
||||||
services.AddAuthentication(authOptions =>
|
|
||||||
{
|
|
||||||
})
|
|
||||||
.AddJwtBearer(options =>
|
|
||||||
{
|
|
||||||
options.TokenValidationParameters = new TokenValidationParameters
|
|
||||||
{
|
|
||||||
IssuerSigningKey = issuerSigningKey,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool LifetimeValidator(DateTime? notBefore,
|
|
||||||
DateTime? expires,
|
|
||||||
SecurityToken securityToken,
|
|
||||||
TokenValidationParameters validationParameters)
|
|
||||||
{
|
|
||||||
return expires != null && expires > DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public class Startup
|
public class Startup
|
||||||
{
|
{
|
||||||
#region Constructors
|
#region Constructors
|
||||||
@@ -72,6 +48,20 @@ namespace Icarus
|
|||||||
|
|
||||||
var connString = Configuration.GetConnectionString("DefaultConnection");
|
var connString = Configuration.GetConnectionString("DefaultConnection");
|
||||||
|
|
||||||
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.RequireHttpsMetadata = false;
|
||||||
|
options.SaveToken = true;
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters()
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidAudience = Configuration["JWT:Audience"],
|
||||||
|
ValidIssuer = Configuration["JWT:Issuer"],
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"]))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
services.AddDbContext<SongContext>(options => options.UseMySQL(connString));
|
services.AddDbContext<SongContext>(options => options.UseMySQL(connString));
|
||||||
services.AddDbContext<AlbumContext>(options => options.UseMySQL(connString));
|
services.AddDbContext<AlbumContext>(options => options.UseMySQL(connString));
|
||||||
services.AddDbContext<ArtistContext>(options => options.UseMySQL(connString));
|
services.AddDbContext<ArtistContext>(options => options.UseMySQL(connString));
|
||||||
@@ -79,24 +69,50 @@ namespace Icarus
|
|||||||
services.AddDbContext<GenreContext>(options => options.UseMySQL(connString));
|
services.AddDbContext<GenreContext>(options => options.UseMySQL(connString));
|
||||||
services.AddDbContext<CoverArtContext>(options => options.UseMySQL(connString));
|
services.AddDbContext<CoverArtContext>(options => options.UseMySQL(connString));
|
||||||
|
|
||||||
services.AddAsymmetricAuthentication(Configuration);
|
|
||||||
|
|
||||||
/**
|
|
||||||
services.AddTransient<AuthenticationService>(au => new AuthenticationService(new UserService(Configuration), new TokenService(Configuration), Configuration));
|
|
||||||
services.AddTransient<UserService>(us => new UserService(Configuration));
|
|
||||||
services.AddTransient<TokenService>(tk => new TokenService(Configuration));
|
|
||||||
services.AddTransient<UserRepository>();
|
|
||||||
services.AddTransient<UserContext>(uc => new UserContext(connString));
|
|
||||||
*/
|
|
||||||
|
|
||||||
services.AddControllers()
|
services.AddControllers()
|
||||||
.AddNewtonsoftJson();
|
.AddNewtonsoftJson();
|
||||||
|
|
||||||
|
services.AddEndpointsApiExplorer();
|
||||||
|
services.AddSwaggerGen(c =>
|
||||||
|
{
|
||||||
|
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
|
||||||
|
|
||||||
|
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Icarus", Version = "v1" });
|
||||||
|
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
|
||||||
|
{
|
||||||
|
Name = "Authorization",
|
||||||
|
Scheme = "Bearer",
|
||||||
|
BearerFormat = "JWT",
|
||||||
|
Type = SecuritySchemeType.ApiKey,
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Description = "Bearer *Auth Token*",
|
||||||
|
});
|
||||||
|
c.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Type = ReferenceType.SecurityScheme,
|
||||||
|
Id = "Bearer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new string[] {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called by the runtime. Use this method to configure the HTTP request pipeline.
|
// Called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||||
{
|
{
|
||||||
// NOTE: Dev-related configuration can be done when env.IsDevelopment() evaluated to true
|
// NOTE: Dev-related configuration can be done when env.IsDevelopment() evaluated to true
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
|
|||||||
@@ -16,6 +16,12 @@
|
|||||||
"PrivateKeyPath": "",
|
"PrivateKeyPath": "",
|
||||||
"PublicKeyPath": ""
|
"PublicKeyPath": ""
|
||||||
},
|
},
|
||||||
|
"JWT": {
|
||||||
|
"Issuer": "",
|
||||||
|
"Audience": "",
|
||||||
|
"Secret": "",
|
||||||
|
"Subject": ""
|
||||||
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
|
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,12 @@
|
|||||||
"PrivateKeyPath": "",
|
"PrivateKeyPath": "",
|
||||||
"PublicKeyPath": ""
|
"PublicKeyPath": ""
|
||||||
},
|
},
|
||||||
|
"JWT": {
|
||||||
|
"Issuer": "",
|
||||||
|
"Audience": "",
|
||||||
|
"Secret": "",
|
||||||
|
"Subject": ""
|
||||||
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
|
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user