Pre release #81
@@ -3,6 +3,9 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
/.vs/Icarus
|
/.vs/Icarus
|
||||||
|
/bin/*
|
||||||
/bin/Debug/netcoreapp2.2
|
/bin/Debug/netcoreapp2.2
|
||||||
/Migrations
|
/Migrations
|
||||||
/obj
|
/obj
|
||||||
|
/obj/*
|
||||||
|
/Icarus.txt
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Org.BouncyCastle.Crypto.Parameters;
|
||||||
|
using Org.BouncyCastle.OpenSsl;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Icarus.Certs
|
||||||
|
{
|
||||||
|
public class SigningIssuerCertificate : IDisposable
|
||||||
|
{
|
||||||
|
private readonly RSACryptoServiceProvider _rsa;
|
||||||
|
|
||||||
|
public SigningIssuerCertificate()
|
||||||
|
{
|
||||||
|
_rsa = new RSACryptoServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RsaSecurityKey GetIssuerSigningKey(string publicKeyPath)
|
||||||
|
{
|
||||||
|
var file = publicKeyPath;
|
||||||
|
var publicKey = System.IO.File.ReadAllText(file);
|
||||||
|
|
||||||
|
using (var reader = System.IO.File.OpenText(file))
|
||||||
|
{
|
||||||
|
var pem = new PemReader(reader);
|
||||||
|
var o = (RsaKeyParameters)pem.ReadObject();
|
||||||
|
var parameters = new RSAParameters();
|
||||||
|
parameters.Modulus = o.Modulus.ToByteArray();
|
||||||
|
parameters.Exponent = o.Exponent.ToByteArray();
|
||||||
|
_rsa.ImportParameters(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RsaSecurityKey(_rsa);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_rsa?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class SigningAudienceCertificate : IDisposable
|
||||||
|
{
|
||||||
|
private readonly RSACryptoServiceProvider _rsa;
|
||||||
|
|
||||||
|
public SigningAudienceCertificate()
|
||||||
|
{
|
||||||
|
_rsa = new RSACryptoServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SigningCredentials GetAudienceSigningKey(string keyPath)
|
||||||
|
{
|
||||||
|
var file = keyPath;
|
||||||
|
var publicKey = System.IO.File.ReadAllText(file);
|
||||||
|
|
||||||
|
using (var reader = System.IO.File.OpenText(file))
|
||||||
|
{
|
||||||
|
var pem = new PemReader(reader);
|
||||||
|
var o = (RsaKeyParameters)pem.ReadObject();
|
||||||
|
var parameters = new RSAParameters();
|
||||||
|
parameters.Modulus = o.Modulus.ToByteArray();
|
||||||
|
parameters.Exponent = o.Exponent.ToByteArray();
|
||||||
|
_rsa.ImportParameters(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SigningCredentials(
|
||||||
|
key: new RsaSecurityKey(_rsa),
|
||||||
|
algorithm: SecurityAlgorithms.RsaSha256);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_rsa?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -180,23 +180,30 @@ namespace Icarus.Controllers.Managers
|
|||||||
DirectoryManager dirMgr = new DirectoryManager(_config, song);
|
DirectoryManager dirMgr = new DirectoryManager(_config, song);
|
||||||
dirMgr.CreateDirectory();
|
dirMgr.CreateDirectory();
|
||||||
|
|
||||||
var filePath = dirMgr.SongDirectory;
|
var tempPath = song.SongPath();
|
||||||
filePath += $"{song.Filename}";
|
song.Filename = song.GenerateFilename(1);
|
||||||
|
var filePath = $"{dirMgr.SongDirectory}{song.Filename}";
|
||||||
|
|
||||||
_logger.Info($"Absolute song path: {filePath}");
|
_logger.Info($"Absolute song path: {filePath}");
|
||||||
|
|
||||||
|
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
using (var fileStream = new FileStream(filePath, FileMode.Create))
|
using (var fileStream = new FileStream(filePath, FileMode.Create))
|
||||||
{
|
{
|
||||||
var songBytes = await System.IO.File.ReadAllBytesAsync(song.SongPath());
|
var songBytes = System.IO.File.ReadAllBytes(tempPath);
|
||||||
|
|
||||||
System.IO.File.WriteAllBytesAsync(filePath, songBytes);
|
_logger.Info("Saving song to the filesystem");
|
||||||
System.IO.File.Delete(song.SongPath());
|
fileStream.Write(songBytes, 0, songBytes.Count());
|
||||||
|
|
||||||
song.SongDirectory = dirMgr.SongDirectory;
|
System.IO.File.Delete(tempPath);
|
||||||
|
_logger.Info("Deleting temp file");
|
||||||
|
|
||||||
_logger.Info("Song successfully saved to filesystem");
|
_logger.Info("Song successfully saved to filesystem");
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
song.SongDirectory = dirMgr.SongDirectory;
|
||||||
|
|
||||||
var coverMgr = new CoverArtManager(_config);
|
var coverMgr = new CoverArtManager(_config);
|
||||||
var coverArt = coverMgr.SaveCoverArt(song);
|
var coverArt = coverMgr.SaveCoverArt(song);
|
||||||
@@ -211,51 +218,74 @@ namespace Icarus.Controllers.Managers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveSongToFileSystem(IFormFile songFile, IFormFile coverArtData, Song song)
|
public void SaveSongToFileSystem(IFormFile songFile, IFormFile coverArtData, Song song)
|
||||||
{
|
{
|
||||||
|
song.SongDirectory = _tempDirectoryRoot;
|
||||||
|
song.DateCreated = DateTime.Now;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(song.Filename))
|
if (string.IsNullOrEmpty(song.Filename))
|
||||||
{
|
{
|
||||||
song.Filename = song.GenerateFilename(1);
|
song.Filename = song.GenerateFilename(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
song.SongDirectory = _tempDirectoryRoot;
|
_logger.Info($"Temporary directory: {_tempDirectoryRoot}");
|
||||||
song.DateCreated = DateTime.Now;
|
|
||||||
|
|
||||||
using (var filestream = new FileStream(song.SongPath(), FileMode.Create))
|
var tempPath = song.SongPath();
|
||||||
|
|
||||||
|
_logger.Info("Temporary song path: {0}", tempPath);
|
||||||
|
|
||||||
|
using (var filestream = new FileStream(tempPath, FileMode.Create))
|
||||||
{
|
{
|
||||||
_logger.Info("Saving song to temporary directory");
|
_logger.Info("Saving song to temporary directory");
|
||||||
await songFile.CopyToAsync(filestream);
|
songFile.CopyTo(filestream);
|
||||||
}
|
}
|
||||||
|
|
||||||
var coverMgr = new CoverArtManager(_config);
|
var coverMgr = new CoverArtManager(_config);
|
||||||
var coverArt = coverMgr.SaveCoverArt(coverArtData, song);
|
|
||||||
|
|
||||||
var meta = new MetadataRetriever();
|
var meta = new MetadataRetriever();
|
||||||
|
var coverArt = coverMgr.SaveCoverArt(coverArtData, song);
|
||||||
|
meta.UpdateCoverArt(song, coverArt);
|
||||||
song.Duration = meta.RetrieveSongDuration(song.SongPath());
|
song.Duration = meta.RetrieveSongDuration(song.SongPath());
|
||||||
|
|
||||||
meta.UpdateMetadata(song, song);
|
meta.UpdateMetadata(song, song);
|
||||||
meta.UpdateCoverArt(song, coverArt);
|
|
||||||
|
|
||||||
DirectoryManager dirMgr = new DirectoryManager(_config, song);
|
DirectoryManager dirMgr = new DirectoryManager(_config, song);
|
||||||
dirMgr.CreateDirectory();
|
dirMgr.CreateDirectory();
|
||||||
|
|
||||||
var filePath = dirMgr.SongDirectory + song.Filename;
|
song.SongDirectory = dirMgr.SongDirectory;
|
||||||
|
|
||||||
|
var filePath = song.SongPath();
|
||||||
_logger.Info($"Absolute song path: {filePath}");
|
_logger.Info($"Absolute song path: {filePath}");
|
||||||
|
|
||||||
|
|
||||||
using (var fileStream = new FileStream(filePath, FileMode.Create))
|
using (var fileStream = new FileStream(filePath, FileMode.Create))
|
||||||
{
|
{
|
||||||
var songBytes = await System.IO.File.ReadAllBytesAsync(song.SongPath());
|
var songBytes = System.IO.File.ReadAllBytes(tempPath);
|
||||||
|
|
||||||
await System.IO.File.WriteAllBytesAsync(filePath, songBytes);
|
try
|
||||||
System.IO.File.Delete(song.SongPath());
|
{
|
||||||
song.SongDirectory = dirMgr.SongDirectory;
|
if (System.IO.File.Exists(filePath) && System.IO.File.Exists(tempPath) && fileStream.Length > 0)
|
||||||
|
{
|
||||||
|
System.IO.File.Delete(tempPath);
|
||||||
|
_logger.Info("Deleted temp song from filesystem: {0}", tempPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fileStream.Write(songBytes, 0, songBytes.Count());
|
||||||
|
_logger.Info("Saved song to filesystem: {0}", filePath);
|
||||||
|
|
||||||
|
System.IO.File.Delete(tempPath);
|
||||||
|
_logger.Info("Deleted temp song from filesystem: {0}", tempPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var msg = ex.Message;
|
||||||
|
_logger.Error($"An error occurred: {msg}");
|
||||||
|
}
|
||||||
|
|
||||||
_logger.Info("Song successfully saved to filesystem");
|
_logger.Info("Song successfully saved to filesystem");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
coverMgr.SaveCoverArtToDatabase(ref song, ref coverArt);
|
coverMgr.SaveCoverArtToDatabase(ref song, ref coverArt);
|
||||||
SaveSongToDatabase(song);
|
SaveSongToDatabase(song);
|
||||||
}
|
}
|
||||||
@@ -296,20 +326,23 @@ namespace Icarus.Controllers.Managers
|
|||||||
var song = new Song();
|
var song = new Song();
|
||||||
_logger.Info("Assigning song filename");
|
_logger.Info("Assigning song filename");
|
||||||
song.SongDirectory = _tempDirectoryRoot;
|
song.SongDirectory = _tempDirectoryRoot;
|
||||||
song.Filename = song.GenerateFilename(1);
|
var filename = song.GenerateFilename(1);
|
||||||
|
song.Filename = filename;
|
||||||
|
|
||||||
using (var filestream = new FileStream(song.SongPath(), FileMode.Create))
|
using (var filestream = new FileStream(song.SongPath(), FileMode.Create))
|
||||||
{
|
{
|
||||||
_logger.Info("Saving song to temporary directory");
|
_logger.Info("Saving temp song: {0}", song.SongPath());
|
||||||
await songFile.CopyToAsync(filestream);
|
await songFile.CopyToAsync(filestream);
|
||||||
}
|
}
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
MetadataRetriever meta = new MetadataRetriever();
|
MetadataRetriever meta = new MetadataRetriever();
|
||||||
song = meta.RetrieveMetaData(song.SongPath());
|
song = meta.RetrieveMetaData(song.SongPath());
|
||||||
|
});
|
||||||
|
|
||||||
song.SongDirectory = _tempDirectoryRoot;
|
song.SongDirectory = _tempDirectoryRoot;
|
||||||
song.Filename = song.GenerateFilename(1);
|
|
||||||
song.DateCreated = DateTime.Now;
|
song.DateCreated = DateTime.Now;
|
||||||
|
song.Filename = filename;
|
||||||
|
|
||||||
return song;
|
return song;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,39 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using JWT;
|
||||||
|
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.Parameters;
|
||||||
|
using Org.BouncyCastle.OpenSsl;
|
||||||
|
using Org.BouncyCastle.Security;
|
||||||
|
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
|
|
||||||
using Icarus.Models;
|
using Icarus.Models;
|
||||||
|
|
||||||
namespace Icarus.Controllers.Managers
|
namespace Icarus.Controllers.Managers
|
||||||
{
|
{
|
||||||
|
#region Classes
|
||||||
public class TokenManager : BaseManager
|
public class TokenManager : BaseManager
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
private string _clientId;
|
private string _clientId;
|
||||||
private string _clientSecret;
|
private string _clientSecret;
|
||||||
|
private string _privateKeyPath;
|
||||||
|
private string _privateKey;
|
||||||
|
private string _publicKeyPath;
|
||||||
|
private string _publicKey;
|
||||||
private string _audience;
|
private string _audience;
|
||||||
private string _grantType;
|
private string _grantType;
|
||||||
private string _url;
|
private string _url;
|
||||||
@@ -58,7 +77,7 @@ namespace Icarus.Controllers.Managers
|
|||||||
|
|
||||||
_logger.Info("Deserializing response");
|
_logger.Info("Deserializing response");
|
||||||
var tokenResult = JsonConvert
|
var tokenResult = JsonConvert
|
||||||
.DeserializeObject<Token>(response.Content);
|
.DeserializeObject<TokenTierOne>(response.Content);
|
||||||
_logger.Info("Response deserialized");
|
_logger.Info("Response deserialized");
|
||||||
|
|
||||||
return new LoginResult
|
return new LoginResult
|
||||||
@@ -69,6 +88,259 @@ namespace Icarus.Controllers.Managers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public LoginResult LogIn(User user)
|
||||||
|
{
|
||||||
|
var tokenResult = new TokenTierOne();
|
||||||
|
tokenResult.TokenType = "Jwt";
|
||||||
|
|
||||||
|
var privateKey = ReadKeyContent(_privateKeyPath).Result;
|
||||||
|
var publicKey = ReadKeyContent(_publicKeyPath).Result;
|
||||||
|
|
||||||
|
var payload = Payload();
|
||||||
|
|
||||||
|
var token = CreateToken(payload, privateKey);
|
||||||
|
tokenResult.AccessToken = token;
|
||||||
|
|
||||||
|
var expClaim = payload.FirstOrDefault(cl =>
|
||||||
|
{
|
||||||
|
return cl.Type.Equals("exp");
|
||||||
|
});
|
||||||
|
|
||||||
|
tokenResult.Expiration = System.Convert.ToInt32(expClaim.Value);
|
||||||
|
|
||||||
|
return new LoginResult
|
||||||
|
{
|
||||||
|
UserID = user.UserID, Username = user.Username, Token = tokenResult.AccessToken,
|
||||||
|
TokenType = tokenResult.TokenType, Expiration = tokenResult.Expiration,
|
||||||
|
Message = "Successfully retrieved token"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var result = false;
|
||||||
|
var token = DecodeToken(accessToken);
|
||||||
|
|
||||||
|
if (token == null || token.Erroneous())
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = (!token.TokenExpired() && token.ContainsScope(scope)) ? true : false;
|
||||||
|
|
||||||
|
// What would make a token valid?
|
||||||
|
// 1. The expiration date must be before the current date
|
||||||
|
// 2. The desired scope must be part of the scopes within the access token
|
||||||
|
// 3. Must be able to be decoded
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Token? DecodeToken(string accessToken)
|
||||||
|
{
|
||||||
|
var rsaParams = GetRSAPublic(_publicKey);
|
||||||
|
Token tok = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var rsa = new RSACryptoServiceProvider())
|
||||||
|
{
|
||||||
|
rsa.ImportParameters(rsaParams);
|
||||||
|
|
||||||
|
IJsonSerializer serializer = new JsonNetSerializer();
|
||||||
|
IDateTimeProvider provider = new UtcDateTimeProvider();
|
||||||
|
IJwtValidator validator = new JwtValidator(serializer, provider);
|
||||||
|
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
|
||||||
|
var algorithm = new JWT.Algorithms.RS256Algorithm(rsa);
|
||||||
|
IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
|
||||||
|
|
||||||
|
var json = decoder.Decode(accessToken);
|
||||||
|
tok = JsonConvert.DeserializeObject<Token>(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error("An error occurred: {0}", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return tok;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private string AllScopes()
|
||||||
|
{
|
||||||
|
var allScopes = new List<String>()
|
||||||
|
{
|
||||||
|
"download:songs",
|
||||||
|
"read:song_details",
|
||||||
|
"upload:songs",
|
||||||
|
"delete:songs",
|
||||||
|
"read:albums",
|
||||||
|
"read:artists",
|
||||||
|
"update:songs",
|
||||||
|
"stream:songs",
|
||||||
|
"read:genre",
|
||||||
|
"read:year",
|
||||||
|
"download:cover_art"
|
||||||
|
};
|
||||||
|
|
||||||
|
var scopes = string.Empty;
|
||||||
|
|
||||||
|
for (var i = 0; i < allScopes.Count; i++)
|
||||||
|
{
|
||||||
|
if (i == allScopes.Count - 1)
|
||||||
|
{
|
||||||
|
scopes += allScopes[i];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
scopes += allScopes[i] + " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Claim> Payload()
|
||||||
|
{
|
||||||
|
var expLimit = 30;
|
||||||
|
var currentDate = DateTime.Now;
|
||||||
|
var expiredDate = currentDate.AddMinutes(expLimit);
|
||||||
|
var issued = Math.Floor((currentDate - DateTime.UnixEpoch).TotalSeconds);
|
||||||
|
var expires = Math.Floor((expiredDate - DateTime.UnixEpoch).TotalSeconds);
|
||||||
|
var issuer = "https://soaricarus.auth0.com";
|
||||||
|
issuer = "http://localhost:5002";
|
||||||
|
var audience = "https://icarus/api";
|
||||||
|
audience = "http://localhost:5002";
|
||||||
|
var subject = _config["JWT:Subject"];
|
||||||
|
|
||||||
|
var claim = new List<System.Security.Claims.Claim>()
|
||||||
|
{
|
||||||
|
new System.Security.Claims.Claim("scope", AllScopes(), "string"),
|
||||||
|
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Exp, expiredDate.ToString()),
|
||||||
|
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Aud, audience),
|
||||||
|
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Iss, issuer),
|
||||||
|
new Claim(JwtRegisteredClaimNames.Sub, subject),
|
||||||
|
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Iat, currentDate.ToString())
|
||||||
|
};
|
||||||
|
|
||||||
|
return claim;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CreateToken(List<Claim> claims, string privateKey)
|
||||||
|
{
|
||||||
|
var token = string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(privateKey))
|
||||||
|
{
|
||||||
|
privateKey = ReadKeyContent(_privateKeyPath).Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
RSAParameters rsaParams;
|
||||||
|
using (var tr = new System.IO.StringReader(privateKey))
|
||||||
|
{
|
||||||
|
var pemReader = new PemReader(tr);
|
||||||
|
var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
|
||||||
|
if (keyPair == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Could not read RSA private key");
|
||||||
|
}
|
||||||
|
var privateRsaParams = keyPair.Private as RsaPrivateCrtKeyParameters;
|
||||||
|
rsaParams = DotNetUtilities.ToRSAParameters(privateRsaParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
|
||||||
|
{
|
||||||
|
var rsaParamsPublic = GetRSAPublic(ReadKeyContent(_publicKeyPath).Result);
|
||||||
|
var rsaPublic = new RSACryptoServiceProvider();
|
||||||
|
|
||||||
|
rsa.ImportParameters(rsaParams);
|
||||||
|
rsaPublic.ImportParameters(rsaParamsPublic);
|
||||||
|
|
||||||
|
Dictionary<string, object> payload = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
foreach (var claim in claims)
|
||||||
|
{
|
||||||
|
var type = claim.Type;
|
||||||
|
var val = Int32.TryParse(claim.Value, out _);
|
||||||
|
|
||||||
|
if (val)
|
||||||
|
{
|
||||||
|
payload.Add(type, Convert.ToInt32(claim.Value));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
payload.Add(type, claim.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var algorithm = new JWT.Algorithms.RS256Algorithm(rsaPublic, rsa);
|
||||||
|
IJsonSerializer serializer = new JsonNetSerializer();
|
||||||
|
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
|
||||||
|
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
|
||||||
|
|
||||||
|
token = encoder.Encode(payload, privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RSAParameters GetRSAPublic(string publicKey)
|
||||||
|
{
|
||||||
|
using (var tr = new System.IO.StringReader(publicKey))
|
||||||
|
{
|
||||||
|
var pemReader = new PemReader(tr);
|
||||||
|
var publicKeyParams = pemReader.ReadObject() as RsaKeyParameters;
|
||||||
|
if (publicKeyParams == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Could not read RSA public key");
|
||||||
|
}
|
||||||
|
return DotNetUtilities.ToRSAParameters(publicKeyParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> ReadKeyContent(string filepath)
|
||||||
|
{
|
||||||
|
return await System.IO.File.ReadAllTextAsync(filepath);
|
||||||
|
}
|
||||||
|
|
||||||
private TokenRequest RetrieveTokenRequest()
|
private TokenRequest RetrieveTokenRequest()
|
||||||
{
|
{
|
||||||
_logger.Info("Retrieving token object");
|
_logger.Info("Retrieving token object");
|
||||||
@@ -89,6 +361,10 @@ namespace Icarus.Controllers.Managers
|
|||||||
_audience = _config["Auth0:ApiIdentifier"];
|
_audience = _config["Auth0:ApiIdentifier"];
|
||||||
_grantType = "client_credentials";
|
_grantType = "client_credentials";
|
||||||
_url = $"https://{_config["Auth0:Domain"]}";
|
_url = $"https://{_config["Auth0:Domain"]}";
|
||||||
|
_privateKeyPath = _config["RSAKeys:PrivateKeyPath"];
|
||||||
|
_publicKeyPath = _config["RSAKeys:PublicKeyPath"];
|
||||||
|
_privateKey = System.IO.File.ReadAllText(_privateKeyPath);
|
||||||
|
_publicKey = System.IO.File.ReadAllText(_publicKeyPath);
|
||||||
|
|
||||||
PrintCredentials();
|
PrintCredentials();
|
||||||
}
|
}
|
||||||
@@ -119,7 +395,7 @@ namespace Icarus.Controllers.Managers
|
|||||||
[JsonProperty("grant_type")]
|
[JsonProperty("grant_type")]
|
||||||
public string GrantType { get; set; }
|
public string GrantType { get; set; }
|
||||||
}
|
}
|
||||||
private class Token
|
private class TokenTierOne
|
||||||
{
|
{
|
||||||
[JsonProperty("access_token")]
|
[JsonProperty("access_token")]
|
||||||
public string AccessToken { get; set; }
|
public string AccessToken { get; set; }
|
||||||
@@ -130,4 +406,5 @@ namespace Icarus.Controllers.Managers
|
|||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Configuration;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -10,18 +9,17 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
using Icarus.Models;
|
using Icarus.Models;
|
||||||
using Icarus.Database.Contexts;
|
using Icarus.Database.Contexts;
|
||||||
// using Icarus.Database.Repositories;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.V1
|
namespace Icarus.Controllers.V1
|
||||||
{
|
{
|
||||||
[Route("api/v1/album")]
|
[Route("api/v1/album")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class AlbumController : ControllerBase
|
[Authorize]
|
||||||
|
public class AlbumController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
private readonly ILogger<AlbumController> _logger;
|
private readonly ILogger<AlbumController> _logger;
|
||||||
private string _connectionString;
|
private string _connectionString;
|
||||||
private IConfiguration _config;
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
@@ -41,8 +39,7 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
#region HTTP Routes
|
#region HTTP Routes
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize("read:albums")]
|
public IActionResult GetAlbums()
|
||||||
public IActionResult Get()
|
|
||||||
{
|
{
|
||||||
List<Album> albums = new List<Album>();
|
List<Album> albums = new List<Album>();
|
||||||
|
|
||||||
@@ -57,8 +54,7 @@ namespace Icarus.Controllers.V1
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[Authorize("read:albums")]
|
public IActionResult GetAlbum(int id)
|
||||||
public IActionResult Get(int id)
|
|
||||||
{
|
{
|
||||||
Album album = new Album
|
Album album = new Album
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Configuration;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -15,12 +13,12 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[Route("api/v1/artist")]
|
[Route("api/v1/artist")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class ArtistController : ControllerBase
|
[Authorize]
|
||||||
|
public class ArtistController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
private readonly ILogger<ArtistController> _logger;
|
private readonly ILogger<ArtistController> _logger;
|
||||||
private string _connectionString;
|
private string _connectionString;
|
||||||
private IConfiguration _config;
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
@@ -40,8 +38,7 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
#region HTTP Routes
|
#region HTTP Routes
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize("read:artists")]
|
public IActionResult GetArtists()
|
||||||
public IActionResult Get()
|
|
||||||
{
|
{
|
||||||
var artistContext = new ArtistContext(_connectionString);
|
var artistContext = new ArtistContext(_connectionString);
|
||||||
|
|
||||||
@@ -54,9 +51,13 @@ namespace Icarus.Controllers.V1
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[Authorize("read:artists")]
|
public IActionResult GetArtist(int id)
|
||||||
public IActionResult Get(int id)
|
|
||||||
{
|
{
|
||||||
|
if (!IsTokenValid("read:artists"))
|
||||||
|
{
|
||||||
|
return StatusCode(401, "Not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
Artist artist = new Artist
|
Artist artist = new Artist
|
||||||
{
|
{
|
||||||
ArtistID = id
|
ArtistID = id
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
using Icarus.Controllers.Managers;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Icarus.Controllers.V1
|
||||||
|
{
|
||||||
|
public class BaseController : ControllerBase
|
||||||
|
{
|
||||||
|
#region Fiends
|
||||||
|
protected IConfiguration _config;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Methods
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
|
protected string ParseBearerTokenFromHeader()
|
||||||
|
{
|
||||||
|
var token = string.Empty;
|
||||||
|
const string tokenType = "Bearer";
|
||||||
|
const string otherTokenType = "Jwt";
|
||||||
|
|
||||||
|
var req = Request;
|
||||||
|
var auth = req.Headers.Authorization;
|
||||||
|
var val = auth.ToString();
|
||||||
|
|
||||||
|
if ((val.Contains(tokenType) || val.Contains(otherTokenType)) && val.Split(" ").Count() > 1)
|
||||||
|
{
|
||||||
|
var split = val.Split(" ");
|
||||||
|
token = split[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
|
protected bool IsTokenValid(string scope)
|
||||||
|
{
|
||||||
|
var token = ParseBearerTokenFromHeader();
|
||||||
|
var tokMgr = new TokenManager(_config);
|
||||||
|
|
||||||
|
return tokMgr.IsTokenValid(scope, token);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -17,12 +15,12 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[Route("api/v1/coverart")]
|
[Route("api/v1/coverart")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class CoverArtController : ControllerBase
|
[Authorize]
|
||||||
|
public class CoverArtController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
private readonly ILogger<CoverArtController> _logger;
|
private readonly ILogger<CoverArtController> _logger;
|
||||||
private string _connectionString;
|
private string _connectionString;
|
||||||
private IConfiguration _config;
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
@@ -37,7 +35,8 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
|
|
||||||
#region HTTP Routes
|
#region HTTP Routes
|
||||||
public IActionResult Get()
|
[HttpGet]
|
||||||
|
public IActionResult GetCoverArts()
|
||||||
{
|
{
|
||||||
var coverArtContext = new CoverArtContext(_connectionString);
|
var coverArtContext = new CoverArtContext(_connectionString);
|
||||||
|
|
||||||
@@ -56,8 +55,7 @@ namespace Icarus.Controllers.V1
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[Authorize("download:cover_art")]
|
public IActionResult GetCoverArt(int id)
|
||||||
public async Task<IActionResult> Get(int id)
|
|
||||||
{
|
{
|
||||||
var coverArt = new CoverArt { CoverArtID = id };
|
var coverArt = new CoverArt { CoverArtID = id };
|
||||||
|
|
||||||
@@ -68,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,12 +14,12 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[Route("api/v1/genre")]
|
[Route("api/v1/genre")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class GenreController : ControllerBase
|
[Authorize]
|
||||||
|
public class GenreController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
private readonly ILogger<GenreController> _logger;
|
private readonly ILogger<GenreController> _logger;
|
||||||
private string _connectionString;
|
private string _connectionString;
|
||||||
private IConfiguration _config;
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
@@ -39,8 +39,7 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
#region HTTP Routes
|
#region HTTP Routes
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize("read:genre")]
|
public IActionResult GetGenres()
|
||||||
public IActionResult Get()
|
|
||||||
{
|
{
|
||||||
var genres = new List<Genre>();
|
var genres = new List<Genre>();
|
||||||
|
|
||||||
@@ -55,8 +54,7 @@ namespace Icarus.Controllers.V1
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[Authorize("read:genre")]
|
public IActionResult GetGenre(int id)
|
||||||
public IActionResult Get(int id)
|
|
||||||
{
|
{
|
||||||
var genre = new Genre
|
var genre = new Genre
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,6 +53,8 @@ namespace Icarus.Controllers.V1
|
|||||||
Username = user.Username
|
Username = user.Username
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
if (context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username)) != null)
|
if (context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username)) != null)
|
||||||
{
|
{
|
||||||
user = context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username));
|
user = context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username));
|
||||||
@@ -73,7 +73,7 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
TokenManager tk = new TokenManager(_config);
|
TokenManager tk = new TokenManager(_config);
|
||||||
|
|
||||||
loginRes = tk.RetrieveLoginResult(user);
|
loginRes = tk.LoginSymmetric(user);
|
||||||
|
|
||||||
return Ok(loginRes);
|
return Ok(loginRes);
|
||||||
}
|
}
|
||||||
@@ -84,6 +84,14 @@ namespace Icarus.Controllers.V1
|
|||||||
return NotFound(loginRes);
|
return NotFound(loginRes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError("An error occurred: {0}", ex.Message);
|
||||||
|
_logger.LogError("Inner Exception: {0}", ex.InnerException.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,11 +19,11 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[Route("api/v1/song/compressed/data")]
|
[Route("api/v1/song/compressed/data")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class SongCompressedDataController : ControllerBase
|
[Authorize]
|
||||||
|
public class SongCompressedDataController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
private string _connectionString;
|
private string _connectionString;
|
||||||
private IConfiguration _config;
|
|
||||||
private string _songTempDir;
|
private string _songTempDir;
|
||||||
private string _archiveDir;
|
private string _archiveDir;
|
||||||
#endregion
|
#endregion
|
||||||
@@ -46,8 +46,7 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
#region API Routes
|
#region API Routes
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[Authorize("download:songs")]
|
public async Task<IActionResult> DownloadCompressedSong(int id)
|
||||||
public async Task<IActionResult> Get(int id)
|
|
||||||
{
|
{
|
||||||
var context = new SongContext(_connectionString);
|
var context = new SongContext(_connectionString);
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[Route("api/v1/song")]
|
[Route("api/v1/song")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class SongController : ControllerBase
|
[Authorize]
|
||||||
|
public class SongController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
private readonly ILogger<SongController> _logger;
|
private readonly ILogger<SongController> _logger;
|
||||||
private string _connectionString;
|
private string _connectionString;
|
||||||
private IConfiguration _config;
|
|
||||||
private SongManager _songMgr;
|
private SongManager _songMgr;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -43,9 +43,12 @@ namespace Icarus.Controllers.V1
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Methods
|
||||||
|
#region HTTP Endpoints
|
||||||
|
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize("read:song_details")]
|
public IActionResult GetSongs()
|
||||||
public IActionResult Get()
|
|
||||||
{
|
{
|
||||||
List<Song> songs = new List<Song>();
|
List<Song> songs = new List<Song>();
|
||||||
Console.WriteLine("Attemtping to retrieve songs");
|
Console.WriteLine("Attemtping to retrieve songs");
|
||||||
@@ -62,8 +65,7 @@ namespace Icarus.Controllers.V1
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[Authorize("read:song_details")]
|
public IActionResult GetSong(int id)
|
||||||
public IActionResult Get(int id)
|
|
||||||
{
|
{
|
||||||
var context = new SongContext(_connectionString);
|
var context = new SongContext(_connectionString);
|
||||||
|
|
||||||
@@ -78,9 +80,8 @@ namespace Icarus.Controllers.V1
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize("update:songs")]
|
|
||||||
[HttpPut("{id}")]
|
[HttpPut("{id}")]
|
||||||
public IActionResult Put(int id, [FromBody] Song song)
|
public IActionResult UpdateSong(int id, [FromBody] Song song)
|
||||||
{
|
{
|
||||||
var context = new SongContext(_connectionString);
|
var context = new SongContext(_connectionString);
|
||||||
|
|
||||||
@@ -100,5 +101,7 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
return Ok(songRes);
|
return Ok(songRes);
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[Route("api/v1/song/data")]
|
[Route("api/v1/song/data")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class SongDataController : ControllerBase
|
[Authorize]
|
||||||
|
public class SongDataController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
private string _connectionString;
|
private string _connectionString;
|
||||||
private IConfiguration _config;
|
|
||||||
private ILogger<SongDataController> _logger;
|
private ILogger<SongDataController> _logger;
|
||||||
private SongManager _songMgr;
|
private SongManager _songMgr;
|
||||||
private string _songTempDir;
|
private string _songTempDir;
|
||||||
@@ -47,16 +47,13 @@ namespace Icarus.Controllers.V1
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("download/{id}")]
|
[HttpGet("download/{id}")]
|
||||||
[Route("private-scoped")]
|
public IActionResult Download(int id)
|
||||||
[Authorize("download:songs")]
|
|
||||||
public async Task<IActionResult> Get(int id)
|
|
||||||
{
|
{
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -74,25 +71,24 @@ 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)
|
||||||
[Authorize("upload:songs")]
|
|
||||||
public async Task<IActionResult> Post([FromForm(Name = "file")] List<IFormFile> songData)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Console.WriteLine("Uploading song...");
|
// Console.WriteLine("Uploading song...");
|
||||||
_logger.LogInformation("Uploading song...");
|
_logger.LogInformation("Uploading song...");
|
||||||
|
|
||||||
var uploads = _songTempDir;
|
var uploads = _songTempDir;
|
||||||
Console.WriteLine($"Song Root Path {uploads}");
|
// Console.WriteLine($"Song Root Path {uploads}");
|
||||||
_logger.LogInformation($"Song root path {uploads}");
|
_logger.LogInformation($"Song root path {uploads}");
|
||||||
|
|
||||||
foreach (var sng in songData)
|
foreach (var sng in songData)
|
||||||
if (sng.Length > 0) {
|
if (sng.Length > 0)
|
||||||
Console.WriteLine($"Song filename {sng.FileName}");
|
{
|
||||||
|
// Console.WriteLine($"Song filename {sng.FileName}");
|
||||||
_logger.LogInformation($"Song filename {sng.FileName}");
|
_logger.LogInformation($"Song filename {sng.FileName}");
|
||||||
|
|
||||||
await _songMgr.SaveSongToFileSystem(sng);
|
_songMgr.SaveSongToFileSystem(sng).Wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
@@ -111,9 +107,7 @@ 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)
|
||||||
[Authorize("upload:songs")]
|
|
||||||
public async Task<IActionResult> Post ([FromForm] UploadSongWithDataForm up)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -122,7 +116,7 @@ namespace Icarus.Controllers.V1
|
|||||||
var song = Newtonsoft.Json.JsonConvert.DeserializeObject<Song>(up.SongFile);
|
var song = Newtonsoft.Json.JsonConvert.DeserializeObject<Song>(up.SongFile);
|
||||||
_logger.LogInformation($"Song title: {song.Title}");
|
_logger.LogInformation($"Song title: {song.Title}");
|
||||||
|
|
||||||
await _songMgr.SaveSongToFileSystem(up.SongData, up.CoverArtData, song);
|
_songMgr.SaveSongToFileSystem(up.SongData, up.CoverArtData, song);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -134,8 +128,7 @@ namespace Icarus.Controllers.V1
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("delete/{id}")]
|
[HttpDelete("delete/{id}")]
|
||||||
[Authorize("delete:songs")]
|
public IActionResult DeleteSong(int id)
|
||||||
public IActionResult Delete(int id)
|
|
||||||
{
|
{
|
||||||
var songContext = new SongContext(_connectionString);
|
var songContext = new SongContext(_connectionString);
|
||||||
|
|
||||||
@@ -164,7 +157,7 @@ namespace Icarus.Controllers.V1
|
|||||||
{
|
{
|
||||||
[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")]
|
||||||
|
|||||||
@@ -13,18 +13,19 @@ using Microsoft.Extensions.Configuration;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
using Icarus.Models;
|
using Icarus.Models;
|
||||||
|
using Icarus.Controllers.Managers;
|
||||||
using Icarus.Database.Contexts;
|
using Icarus.Database.Contexts;
|
||||||
|
|
||||||
namespace Icarus.Controllers.V1
|
namespace Icarus.Controllers.V1
|
||||||
{
|
{
|
||||||
[Route("api/v1/song/stream")]
|
[Route("api/v1/song/stream")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class SongStreamController : ControllerBase
|
[Authorize]
|
||||||
|
public class SongStreamController : BaseController
|
||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
private ILogger<SongStreamController> _logger;
|
private ILogger<SongStreamController> _logger;
|
||||||
private string _connectionString;
|
private string _connectionString;
|
||||||
private IConfiguration _config;
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
@@ -44,8 +45,7 @@ namespace Icarus.Controllers.V1
|
|||||||
|
|
||||||
#region HTTP endpoints
|
#region HTTP endpoints
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
[Authorize("stream:songs")]
|
public async Task<IActionResult> StreamSong(int id)
|
||||||
public async Task<IActionResult> Get(int id)
|
|
||||||
{
|
{
|
||||||
var context = new SongContext(_config.GetConnectionString("DefaultConnection"));
|
var context = new SongContext(_config.GetConnectionString("DefaultConnection"));
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Linq;
|
|||||||
// using MySql.Data.Entity;
|
// using MySql.Data.Entity;
|
||||||
// using MySql.Data.MySqlClient;
|
// using MySql.Data.MySqlClient;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
using Icarus.Models;
|
using Icarus.Models;
|
||||||
|
|
||||||
@@ -18,11 +19,15 @@ namespace Icarus.Database.Contexts
|
|||||||
public DbSet<User> Users { get; set; }
|
public DbSet<User> Users { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
public UserContext(DbContextOptions<UserContext> options) : base(options) { }
|
public UserContext(DbContextOptions<UserContext> options) : base(options) { }
|
||||||
|
[ActivatorUtilitiesConstructor]
|
||||||
public UserContext(string connString) : base(new DbContextOptionsBuilder<UserContext>()
|
public UserContext(string connString) : base(new DbContextOptionsBuilder<UserContext>()
|
||||||
.UseMySQL(connString).Options)
|
.UseMySQL(connString).Options)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
|||||||
+4
-1
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6</TargetFramework>
|
<TargetFramework>net6</TargetFramework>
|
||||||
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
|
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
|
||||||
<Version>0.1.9</Version>
|
<Version>0.1.10</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
<PackageReference Include="DotNetZip" Version="1.15.0" />
|
<PackageReference Include="DotNetZip" Version="1.15.0" />
|
||||||
<PackageReference Include="EntityFramework" Version="6.4.4" />
|
<PackageReference Include="EntityFramework" Version="6.4.4" />
|
||||||
<PackageReference Include="ID3" Version="0.6.0" />
|
<PackageReference Include="ID3" Version="0.6.0" />
|
||||||
|
<PackageReference Include="JWT" Version="9.0.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.8" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.8" />
|
||||||
@@ -21,8 +22,10 @@
|
|||||||
<PackageReference Include="MySql.EntityFrameworkCore" Version="5.0.8" />
|
<PackageReference Include="MySql.EntityFrameworkCore" Version="5.0.8" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="NLog.Web.AspNetCore" Version="4.14.0" />
|
<PackageReference Include="NLog.Web.AspNetCore" Version="4.14.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>
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.2.32616.157
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Icarus", "Icarus.csproj", "{66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {0BE61157-7A0C-41CF-B097-71178A3F6755}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
@@ -79,6 +80,21 @@ namespace Icarus.Models
|
|||||||
s[random.Next(s.Length)]).ToArray());
|
s[random.Next(s.Length)]).ToArray());
|
||||||
var extension = ".mp3";
|
var extension = ".mp3";
|
||||||
|
|
||||||
|
return flag == 0 ? filename : $"{filename}{extension}";
|
||||||
|
}
|
||||||
|
public async Task<string> GenerateFilenameAsync(int flag = 0)
|
||||||
|
{
|
||||||
|
const int length = 25;
|
||||||
|
const string chars = "ABCDEF0123456789";
|
||||||
|
var extension = ".mp3";
|
||||||
|
var random = new Random();
|
||||||
|
var filename = await Task.Run(() =>
|
||||||
|
{
|
||||||
|
return new string(Enumerable.Repeat(chars, length).Select(s =>
|
||||||
|
s[random.Next(s.Length)]).ToArray());
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
return flag == 0 ? filename : $"{filename}{extension}";
|
return flag == 0 ? filename : $"{filename}{extension}";
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
|
||||||
|
namespace Icarus.Models
|
||||||
|
{
|
||||||
|
public class Token
|
||||||
|
{
|
||||||
|
#region Properties
|
||||||
|
[JsonProperty("scope")]
|
||||||
|
public string Scope { get; set; }
|
||||||
|
[JsonProperty("exp")]
|
||||||
|
public int Expiration { get; set; }
|
||||||
|
[JsonProperty("aud")]
|
||||||
|
public string Audience { get; set; }
|
||||||
|
[JsonProperty("iss")]
|
||||||
|
public string Issuer { get; set; }
|
||||||
|
[JsonProperty("iat")]
|
||||||
|
public int Issued { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Methods
|
||||||
|
public bool TokenExpired()
|
||||||
|
{
|
||||||
|
var result = false;
|
||||||
|
var currentDate = DateTime.Now;
|
||||||
|
|
||||||
|
var currentDateInSeconds = Math.Floor((currentDate - DateTime.UnixEpoch).TotalSeconds);
|
||||||
|
|
||||||
|
result = (currentDateInSeconds >= Expiration) ? true : false;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ContainsScope(string desiredScope)
|
||||||
|
{
|
||||||
|
var result = false;
|
||||||
|
result = Scope.Contains(desiredScope);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Erroneous()
|
||||||
|
{
|
||||||
|
var result = true;
|
||||||
|
Func<string, bool> isEmpty = a => string.IsNullOrEmpty(a);
|
||||||
|
result = (!isEmpty(Scope) && !isEmpty(Audience) && !isEmpty(Issuer) &&
|
||||||
|
Expiration > 0 && Issued > 0) ? false : true;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ namespace Icarus.Models
|
|||||||
[Table("User")]
|
[Table("User")]
|
||||||
public class User
|
public class User
|
||||||
{
|
{
|
||||||
|
#region Properties
|
||||||
[JsonProperty("user_id")]
|
[JsonProperty("user_id")]
|
||||||
[Column("UserID")]
|
[Column("UserID")]
|
||||||
[Key]
|
[Key]
|
||||||
@@ -35,5 +37,21 @@ namespace Icarus.Models
|
|||||||
public string Status { get; set; }
|
public string Status { get; set; }
|
||||||
[JsonProperty("last_login")]
|
[JsonProperty("last_login")]
|
||||||
public DateTime? LastLogin { get; set; }
|
public DateTime? LastLogin { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[NotMapped]
|
||||||
|
public System.Collections.Generic.IEnumerable<string> Roles { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Methods
|
||||||
|
public System.Collections.Generic.IEnumerable<System.Security.Claims.Claim> Claims()
|
||||||
|
{
|
||||||
|
var claims = new System.Collections.Generic.List<System.Security.Claims.Claim> { new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, Username) };
|
||||||
|
claims.AddRange(Roles.Select(role => new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Role, role)));
|
||||||
|
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -10,75 +10,67 @@ One can interface with Icarus the music server either by:
|
|||||||
* [IcarusDownloadManager](https://github.com/amazing-username/IcarusDownloadManager)
|
* [IcarusDownloadManager](https://github.com/amazing-username/IcarusDownloadManager)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Built With
|
## Built With
|
||||||
|
|
||||||
|
|
||||||
* C# [.NET](https://dotnet.microsoft.com/) 6
|
* C# [.NET](https://dotnet.microsoft.com/) 6
|
||||||
* [MySql](https://www.nuget.org/packages/MySql.Data/)
|
* [MySql](https://www.nuget.org/packages/MySql.Data/)
|
||||||
|
* OpenSSL
|
||||||
|
* BCrypt.Net-Next
|
||||||
|
* DotNetZip
|
||||||
|
* ID3
|
||||||
|
* JWT
|
||||||
|
* Microsoft.AspNetCore.Authentication.JwtBearer
|
||||||
|
* Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
||||||
|
* Microsoft.EntityFrameworkCore
|
||||||
|
* Microsoft.EntityFrameworkCore.Tools
|
||||||
|
* MySql.EntityFrameworkCore
|
||||||
* [Newtonsoft.Json](https://www.newtonsoft.com/json)
|
* [Newtonsoft.Json](https://www.newtonsoft.com/json)
|
||||||
|
* NLog.Web.AspNetCpre
|
||||||
|
* Portable.BouncyCastle
|
||||||
|
* RestSharp
|
||||||
|
* SevenZip
|
||||||
|
* System.IdentityModel.Tokens.Jwt
|
||||||
* [TagLib#](https://github.com/mono/taglib-sharp)
|
* [TagLib#](https://github.com/mono/taglib-sharp)
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 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. Auth0 API configuration
|
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
|
||||||
|
|
||||||
### Auth0 API configuration
|
|
||||||
|
|
||||||
Securing Icarus is required, preventing the API from being publicly accessible. To do so, create an Auth0 account (it's free), for the sake of this section of the documentation, I will not go over how to create an Auth0 account. Once created, create a tentant and proceed to create an API
|
### JWT Information
|
||||||
<h1 align=center>
|
|
||||||
<img src="Images/Configuration/create_api.png" width=100%>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
Create the API and enter an approrpiate name and identified. For the identified, append **api** like in the example
|
Configure JWT information. Notably the Secret
|
||||||
<h1 align="center">
|
|
||||||
<img src="Images/Configuration/enter_api_info.png" width=100%>
|
|
||||||
</h1>
|
|
||||||
Replace [domain] with the domain name of the created tenant. This can be found in the Default App from the Application menu. 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
|
||||||
"Auth0": {
|
"JWT": {
|
||||||
|
"Issuer": "IcarusAPI",
|
||||||
|
"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.
|
||||||
|
|
||||||
|
```Json
|
||||||
|
"Auth0": {
|
||||||
"Domain": "[domain].auth0.com",
|
"Domain": "[domain].auth0.com",
|
||||||
"ApiIdentifier": "https://[identifier]/api"
|
"ApiIdentifier": "https://[identifier]/api"
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
For the sake of this section, I will not go over configuring the API to accept the signing algorithm since it has already been configured in the [Startip](Startup.cs).cs file. Click on permissions to create the permissions for the API.
|
**Note**: The Auth0 section is likely to be changed or removed in future releases.
|
||||||
<h1 align "center">
|
|
||||||
<img src="Images/Configuration/configure_api.png" width=100%>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
The permissions ensure that a validated user can interact with the API with a token that has not expired. Ensure that the permissions match, the description can change but the permission identifier must match.
|
|
||||||
<h1 align="center">
|
|
||||||
<img src="Images/Configuration/permissions.png" width=100%>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
On the left side, click on Application and create a new Application. Choose the Machine to Machine Application
|
|
||||||
<h1 align="center">
|
|
||||||
<img src="Images/Configuration/create_m2m.png" width=100%>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
With the grant permissions you created from the API, enable all the permissions. This is important because if they are not enabled then even with a valid token the request will return 403 (unauthorized)
|
|
||||||
<h1 align="center">
|
|
||||||
<img src="Images/Configuration/authorize_app.png" width=100%>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
From the Application page, copy the client id and client secret. These values will be used for the API to interact with API.
|
|
||||||
<h1 align="center">
|
|
||||||
<img src="Images/Configuration/api_cred.png" width=100%>
|
|
||||||
</h1>
|
|
||||||
Enter the information in the corresponding appsettings json file
|
|
||||||
```Json
|
|
||||||
"Auth0": {
|
|
||||||
"ClientId":"",
|
|
||||||
"ClientSecret":""
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
### API filesystem paths
|
### API filesystem paths
|
||||||
|
|
||||||
@@ -87,12 +79,14 @@ For the purposes of properly uploading, downloading, updating, deleting, and str
|
|||||||
{
|
{
|
||||||
"RootMusicPath": "/home/dev/null/music/",
|
"RootMusicPath": "/home/dev/null/music/",
|
||||||
"TemporaryMusicPath": "/home/dev/null/music/temp/",
|
"TemporaryMusicPath": "/home/dev/null/music/temp/",
|
||||||
"ArchivePath": "/home/dev/null/music/archive/"
|
"ArchivePath": "/home/dev/null/music/archive/",
|
||||||
|
"CoverArtPath": "/home/dev/null/music/coverart/"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
* RootMusicPath - Where music will be stored in the following convention: *`Artist/Album/Songs`*
|
* RootMusicPath - Where music will be stored in the following convention: *`Artist/Album/Songs`*
|
||||||
* TemporaryMusicPath - Where music will be stored when uploding songs to the server until the metadata has been fully parsed and entered into the database. Upon completion the files will be deleted and moved to the appropriate path in the `RootMusicPath`
|
* TemporaryMusicPath - Where music will be stored when uploding songs to the server until the metadata has been fully parsed and entered into the database. Upon completion the files will be deleted and moved to the appropriate path in the `RootMusicPath`
|
||||||
* ArchivePath - When downloading compressed songs this is the path where songs will be compressed prior to dataa being read into memory, deleting the compressed file, and sending the compressed file from memory to the client
|
* ArchivePath - When downloading compressed songs this is the path where songs will be compressed prior to dataa being read into memory, deleting the compressed file, and sending the compressed file from memory to the client
|
||||||
|
* CoverArtPath - Root directory where cover art will be saved to
|
||||||
|
|
||||||
|
|
||||||
**Note**: The `TemporaryMusic` or `ArchivePath` does not have to be located in the `RootMusicPath`. Ensure that the permissions are properly set for all of the paths.
|
**Note**: The `TemporaryMusic` or `ArchivePath` does not have to be located in the `RootMusicPath`. Ensure that the permissions are properly set for all of the paths.
|
||||||
@@ -100,15 +94,15 @@ For the purposes of properly uploading, downloading, updating, deleting, and str
|
|||||||
### Database connection string
|
### Database connection string
|
||||||
|
|
||||||
In order for Database functionality to be operable, there must be a valid connection string and credentials with appropriate permissions. **At the moment there is only support for MySQL**. Depending on your environment `Release` or `Debug` you will need to edit the appsettings.json or appsettings.Development.json accordingly. An example of the fields to change are below:
|
In order for Database functionality to be operable, there must be a valid connection string and credentials with appropriate permissions. **At the moment there is only support for MySQL**. Depending on your environment `Release` or `Debug` you will need to edit the appsettings.json or appsettings.Development.json accordingly. An example of the fields to change are below:
|
||||||
|
|
||||||
```Json
|
```Json
|
||||||
{
|
{
|
||||||
|
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Server=localhost;Database=my_db;Uid=admin;Pwd=toughpassword;"
|
"DefaultConnection": "Server=localhost;Database=my_db;Uid=admin;Pwd=toughpassword;"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
* Server - The address or domain name of the MySQL server
|
* Server - The address or domain name of the MySQL server
|
||||||
* Database - The database name
|
* Database - The database name
|
||||||
* Uid - Username
|
* Uid - Username
|
||||||
@@ -146,15 +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.
|
||||||
|
|
||||||
## Versioning
|
|
||||||
|
|
||||||
* [v0.1](https://github.com/amazing-username/Icarus/releases/tag/v0.1)
|
|
||||||
|
|
||||||
## Authors
|
|
||||||
|
|
||||||
* **Kun Deng** - [amazing-username](https://github.com/amazing-username)
|
|
||||||
|
|
||||||
See also the list of [contributors](https://github.com/amazing-username/Icarus/graphs/contributors) who participated in this project.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
Executable → Regular
+59
-67
@@ -1,42 +1,42 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Text;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.HttpsPolicy;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
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;
|
||||||
using NLog.Web.AspNetCore;
|
using NLog.Web.AspNetCore;
|
||||||
|
|
||||||
using Icarus.Authorization;
|
|
||||||
using Icarus.Authorization.Handlers;
|
|
||||||
using Icarus.Database.Contexts;
|
using Icarus.Database.Contexts;
|
||||||
|
|
||||||
namespace Icarus
|
namespace Icarus
|
||||||
{
|
{
|
||||||
public class Startup
|
public class Startup
|
||||||
{
|
{
|
||||||
|
#region Constructors
|
||||||
public Startup(IConfiguration configuration)
|
public Startup(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
Configuration = configuration;
|
Configuration = configuration;
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Properties
|
||||||
public IConfiguration Configuration { get; }
|
public IConfiguration Configuration { get; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Methods
|
||||||
// This method gets called by the runtime. Use this method to add services to the container.
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
@@ -46,66 +46,21 @@ namespace Icarus
|
|||||||
var domain = $"https://{auth_id}/";
|
var domain = $"https://{auth_id}/";
|
||||||
var audience = Configuration["Auth0:ApiIdentifier"];
|
var audience = Configuration["Auth0:ApiIdentifier"];
|
||||||
|
|
||||||
services
|
|
||||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
||||||
.AddJwtBearer(options =>
|
|
||||||
{
|
|
||||||
options.Authority = domain;
|
|
||||||
options.Audience = audience;
|
|
||||||
});
|
|
||||||
|
|
||||||
services.AddAuthorization(options =>
|
|
||||||
{
|
|
||||||
options.AddPolicy("download:songs", policy =>
|
|
||||||
policy.Requirements
|
|
||||||
.Add(new HasScopeRequirement("download:songs", domain)));
|
|
||||||
|
|
||||||
options.AddPolicy("download:cover_art", policy =>
|
|
||||||
policy.Requirements
|
|
||||||
.Add(new HasScopeRequirement("download:cover_art", domain)));
|
|
||||||
|
|
||||||
options.AddPolicy("upload:songs", policy =>
|
|
||||||
policy.Requirements
|
|
||||||
.Add(new HasScopeRequirement("upload:songs", domain)));
|
|
||||||
|
|
||||||
options.AddPolicy("delete:songs", policy =>
|
|
||||||
policy.Requirements
|
|
||||||
.Add(new HasScopeRequirement("delete:songs", domain)));
|
|
||||||
|
|
||||||
options.AddPolicy("read:song_details", policy =>
|
|
||||||
policy.Requirements
|
|
||||||
.Add(new HasScopeRequirement("read:song_details", domain)));
|
|
||||||
|
|
||||||
options.AddPolicy("update:songs", policy =>
|
|
||||||
policy.Requirements
|
|
||||||
.Add(new HasScopeRequirement("update:songs", domain)));
|
|
||||||
|
|
||||||
options.AddPolicy("read:artists", policy =>
|
|
||||||
policy.Requirements
|
|
||||||
.Add(new HasScopeRequirement("read:artists", domain)));
|
|
||||||
|
|
||||||
options.AddPolicy("read:albums", policy =>
|
|
||||||
policy.Requirements
|
|
||||||
.Add(new HasScopeRequirement("read:albums", domain)));
|
|
||||||
|
|
||||||
options.AddPolicy("read:genre", policy =>
|
|
||||||
policy.Requirements
|
|
||||||
.Add(new HasScopeRequirement("read:genre", domain)));
|
|
||||||
|
|
||||||
options.AddPolicy("read:year", policy =>
|
|
||||||
policy.Requirements
|
|
||||||
.Add(new HasScopeRequirement("read:year", domain)));
|
|
||||||
|
|
||||||
options.AddPolicy("stream:songs", policy =>
|
|
||||||
policy.Requirements
|
|
||||||
.Add(new HasScopeRequirement("stream:songs", domain)));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
services.AddSingleton<IAuthorizationHandler, HasScopeHandler>();
|
|
||||||
|
|
||||||
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));
|
||||||
@@ -116,12 +71,48 @@ namespace Icarus
|
|||||||
|
|
||||||
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();
|
||||||
@@ -131,5 +122,6 @@ namespace Icarus
|
|||||||
endpoints.MapControllers();
|
endpoints.MapControllers();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,16 @@
|
|||||||
"ClientId": "",
|
"ClientId": "",
|
||||||
"ClientSecret": ""
|
"ClientSecret": ""
|
||||||
},
|
},
|
||||||
|
"RSAKeys": {
|
||||||
|
"PrivateKeyPath": "",
|
||||||
|
"PublicKeyPath": ""
|
||||||
|
},
|
||||||
|
"JWT": {
|
||||||
|
"Issuer": "",
|
||||||
|
"Audience": "",
|
||||||
|
"Secret": "",
|
||||||
|
"Subject": ""
|
||||||
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
|
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,6 +12,16 @@
|
|||||||
"ClientId": "",
|
"ClientId": "",
|
||||||
"ClientSecret": ""
|
"ClientSecret": ""
|
||||||
},
|
},
|
||||||
|
"RSAKeys": {
|
||||||
|
"PrivateKeyPath": "",
|
||||||
|
"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