diff --git a/ReallifeGamemode.DataService/Controllers/AuthController.cs b/ReallifeGamemode.DataService/Controllers/AuthController.cs new file mode 100644 index 00000000..ddd17f24 --- /dev/null +++ b/ReallifeGamemode.DataService/Controllers/AuthController.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using ReallifeGamemode.Database.Entities; +using ReallifeGamemode.Database.Models; +using ReallifeGamemode.DataService.Logic; +using ReallifeGamemode.DataService.Types; + +namespace ReallifeGamemode.DataService.Controllers +{ + [ApiController] + [Produces("application/json")] + [Route("DataService/Auth")] + public class AuthController : ControllerBase + { + private readonly JwtTokenGenerator tokenGenerator; + private readonly DatabaseContext dbContext; + + public AuthController(JwtTokenGenerator tokenGenerator, DatabaseContext dbContext) + { + this.tokenGenerator = tokenGenerator; + this.dbContext = dbContext; + } + + [HttpPost("Login")] + public ActionResult Login(LoginRequest request) + { + string hashedPassword = ComputeSha256Hash(request.Password); + User user = dbContext.Users.Where(u => u.Name == request.Username).FirstOrDefault(); + + string token = tokenGenerator.GenerateUserToken(user); + + if(string.IsNullOrEmpty(token)) + { + return Unauthorized(); + } + + return new LoginResponse() + { + Token = token + }; + } + + private string ComputeSha256Hash(string rawData) + { + // Create a SHA256 + using (SHA256 sha256Hash = SHA256.Create()) + { + // ComputeHash - returns byte array + byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(rawData)); + + // Convert byte array to a string + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < bytes.Length; i++) + { + builder.Append(bytes[i].ToString("x2")); + } + return builder.ToString(); + } + } + } +} diff --git a/ReallifeGamemode.DataService/Controllers/UserController.cs b/ReallifeGamemode.DataService/Controllers/UserController.cs new file mode 100644 index 00000000..b0b7108a --- /dev/null +++ b/ReallifeGamemode.DataService/Controllers/UserController.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using ReallifeGamemode.Database.Entities; +using ReallifeGamemode.Database.Models; +using ReallifeGamemode.DataService.Types; + +namespace ReallifeGamemode.DataService.Controllers +{ + [ApiController] + [Authorize] + [Route("DataService/User")] + [Produces("application/json")] + public class UserController : ControllerBase + { + private readonly DatabaseContext dbContext; + + public UserController(DatabaseContext dbContext) + { + this.dbContext = dbContext; + } + + [HttpGet("Data")] + public ActionResult Data() + { + User user = dbContext.Users.Where(u => u.Id == UserId).FirstOrDefault(); + + if(user == null) + { + return NotFound(); + } + + return new GetUserDataResponse() + { + Name = user.Name, + AdminLevel = user.AdminLevel, + RegistrationDate = user.RegistrationDate + }; + } + + private int UserId => int.Parse(User.Claims.Where(c => c.Type == ClaimTypes.Name).FirstOrDefault()?.Value ?? "0"); + } +} diff --git a/ReallifeGamemode.DataService/Logic/JwtTokenGenerator.cs b/ReallifeGamemode.DataService/Logic/JwtTokenGenerator.cs new file mode 100644 index 00000000..7d5786ae --- /dev/null +++ b/ReallifeGamemode.DataService/Logic/JwtTokenGenerator.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using ReallifeGamemode.Database; +using ReallifeGamemode.Database.Entities; +using ReallifeGamemode.Database.Models; + +namespace ReallifeGamemode.DataService.Logic +{ + public class JwtTokenGenerator : LogicBase + { + private ServerConfig config; + + public JwtTokenGenerator(IOptions config, DatabaseContext dbContext) : base(dbContext) + { + this.config = config.Value; + } + + public string GenerateUserToken(User user) + { + if(user == null) + { + return null; + } + + var tokenHandler = new JwtSecurityTokenHandler(); + + var key = Encoding.ASCII.GetBytes(config.TokenSecret); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.Name, user.Id.ToString()), + new Claim(ClaimTypes.Role, user.AdminLevel.ToString()) + }), + Expires = DateTime.Now.AddDays(1), + IssuedAt = DateTime.Now, + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature), + Issuer = "LOGDATASERVICE" + }; + + var token = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor)); + + return token; + } + + public string GetDebugToken(byte[] key) + { + var tokenHandler = new JwtSecurityTokenHandler(); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.Name, 1.ToString()), + new Claim(ClaimTypes.Role, (AdminLevel.PROJEKTLEITUNG).ToString()) + }), + Expires = DateTime.Now.AddDays(1), + IssuedAt = DateTime.Now, + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature), + Issuer = "LOGDATASERVICE" + }; + + var token = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor)); + + return token; + } + } +} diff --git a/ReallifeGamemode.DataService/Logic/LogicBase.cs b/ReallifeGamemode.DataService/Logic/LogicBase.cs new file mode 100644 index 00000000..3fc6cbd4 --- /dev/null +++ b/ReallifeGamemode.DataService/Logic/LogicBase.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using ReallifeGamemode.Database.Models; + +namespace ReallifeGamemode.DataService.Logic +{ + public abstract class LogicBase + { + protected readonly DatabaseContext dbContext; + + public LogicBase(DatabaseContext dbContext) + { + this.dbContext = dbContext; + } + } + + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddLogic(this IServiceCollection services) + { + Type[] types = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(LogicBase)) && !t.IsAbstract).ToArray(); + + foreach(Type type in types) + { + services = services.AddScoped(type); + } + + return services; + } + } +} diff --git a/ReallifeGamemode.DataService/Program.cs b/ReallifeGamemode.DataService/Program.cs new file mode 100644 index 00000000..92e1077b --- /dev/null +++ b/ReallifeGamemode.DataService/Program.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace ReallifeGamemode.DataService +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .UseKestrel(k => + { + k.Listen(IPAddress.Any, 5000); + }) + .Build(); + } +} diff --git a/ReallifeGamemode.DataService/ReallifeGamemode.DataService.csproj b/ReallifeGamemode.DataService/ReallifeGamemode.DataService.csproj new file mode 100644 index 00000000..3d8e0907 --- /dev/null +++ b/ReallifeGamemode.DataService/ReallifeGamemode.DataService.csproj @@ -0,0 +1,34 @@ + + + + netcoreapp2.0 + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + diff --git a/ReallifeGamemode.DataService/ServerConfig.cs b/ReallifeGamemode.DataService/ServerConfig.cs new file mode 100644 index 00000000..b52e73a1 --- /dev/null +++ b/ReallifeGamemode.DataService/ServerConfig.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ReallifeGamemode.DataService +{ + public class ServerConfig + { + public string TokenSecret { get; set; } + } +} diff --git a/ReallifeGamemode.DataService/Startup.cs b/ReallifeGamemode.DataService/Startup.cs new file mode 100644 index 00000000..029a21f4 --- /dev/null +++ b/ReallifeGamemode.DataService/Startup.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using ReallifeGamemode.Database.Models; +using ReallifeGamemode.DataService.Logic; +using Swashbuckle.AspNetCore.Swagger; + + +namespace ReallifeGamemode.DataService +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.Configure(cfg => Configuration.Bind(cfg)); + + services.AddDbContext(db => + { + db.UseMySql(Configuration["ConnectionString"]); + }); + + services.AddLogic(); + + services + .AddMvc() + .AddJsonOptions(j => + { + j.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; + j.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc; + j.SerializerSettings.DateFormatHandling = Newtonsoft.Json.DateFormatHandling.IsoDateFormat; + }); + + var tokenKey = Encoding.UTF8.GetBytes(Configuration["TokenSecret"]); + services.AddAuthentication(o => + { + o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(o => + { + o.RequireHttpsMetadata = false; + o.SaveToken = false; + o.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(tokenKey), + ValidateIssuer = true, + ValidIssuer = "LOGDATASERVICE", + ValidateAudience = false, + ValidateLifetime = true + }; + }); + + string debugToken = null; + +#if DEBUG + debugToken = services.BuildServiceProvider().GetService().GetDebugToken(tokenKey); +#endif + + services.AddSwaggerGen(c => + { + Info info = new Info + { + Title = "GTA:V ControlPanl DataService", + Version = "0.1", + }; + + if(debugToken != null) + { + info.Description = $"Debug-Token: {debugToken}"; + } + + c.SwaggerDoc("DataService", info); + + c.AddSecurityDefinition("Bearer", new ApiKeyScheme + { + Name = "Authorization", + In = "Header", + Type = "apiKey" + }); + + c.AddSecurityRequirement(new Dictionary>() + { + { "Bearer", new string[] { } } + }); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + app.UseAuthentication(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseSwaggerUI(c => + { + c.RoutePrefix = "doc"; + c.SwaggerEndpoint("DataService.json", "DataService"); + }); + + app.UseSwagger(c => + { + c.RouteTemplate = "doc/{documentName}.json"; + }); + + app.UseMvc(); + } + } +} diff --git a/ReallifeGamemode.DataService/Types/GetUserDataResponse.cs b/ReallifeGamemode.DataService/Types/GetUserDataResponse.cs new file mode 100644 index 00000000..1e37aff3 --- /dev/null +++ b/ReallifeGamemode.DataService/Types/GetUserDataResponse.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ReallifeGamemode.Database; + +namespace ReallifeGamemode.DataService.Types +{ + public class GetUserDataResponse + { + public string Name { get; set; } + + public AdminLevel AdminLevel { get; set; } + + public DateTime RegistrationDate { get; set; } + } +} diff --git a/ReallifeGamemode.DataService/Types/Login.cs b/ReallifeGamemode.DataService/Types/Login.cs new file mode 100644 index 00000000..d0557d97 --- /dev/null +++ b/ReallifeGamemode.DataService/Types/Login.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ReallifeGamemode.DataService.Types +{ + public class LoginRequest + { + public string Username { get; set; } + + public string Password { get; set; } + } + + public class LoginResponse + { + public string Token { get; set; } + } +} diff --git a/ReallifeGamemode.DataService/appsettings.json b/ReallifeGamemode.DataService/appsettings.json new file mode 100644 index 00000000..d1f22fce --- /dev/null +++ b/ReallifeGamemode.DataService/appsettings.json @@ -0,0 +1,4 @@ +{ + "TokenSecret": "6!/zz8p!oWfQA5P(D8sJzSMeb?n4DjqP1o3q%MVip/ZHMpw$%9VwsrF§(=u531Ha", + "ConnectionString": "Host=localhost;Port=3306;Database=gtav-devdb;Username=gtav-dev;Password=Test123" +} diff --git a/ReallifeGamemode.Database/Models/DatabaseContext.cs b/ReallifeGamemode.Database/Models/DatabaseContext.cs index ece55d43..ad2c2e4c 100644 --- a/ReallifeGamemode.Database/Models/DatabaseContext.cs +++ b/ReallifeGamemode.Database/Models/DatabaseContext.cs @@ -10,10 +10,13 @@ namespace ReallifeGamemode.Database.Models { public partial class DatabaseContext : DbContext { + public DatabaseContext(DbContextOptions options) : base(options) { } + + public DatabaseContext() { } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); - optionsBuilder.UseMySql("Host=localhost;Port=3306;Database=gtav-devdb;Username=gtav-dev;Password=Test123"); } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/ReallifeGamemode.sln b/ReallifeGamemode.sln index 1f3f54de..c6fddcdd 100644 --- a/ReallifeGamemode.sln +++ b/ReallifeGamemode.sln @@ -22,6 +22,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReallifeGamemode.Database", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReallifeGamemode.Services", "ReallifeGamemode.Services\ReallifeGamemode.Services.csproj", "{2EFCB9CA-E9B3-4EA0-BBD8-9F59D2E734D6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReallifeGamemode.DataService", "ReallifeGamemode.DataService\ReallifeGamemode.DataService.csproj", "{61157B05-135C-40C5-B694-5F27CE53B334}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -80,6 +82,18 @@ Global {2EFCB9CA-E9B3-4EA0-BBD8-9F59D2E734D6}.ServerBuild|Any CPU.Build.0 = Debug|Any CPU {2EFCB9CA-E9B3-4EA0-BBD8-9F59D2E734D6}.ServerBuild|x64.ActiveCfg = Debug|Any CPU {2EFCB9CA-E9B3-4EA0-BBD8-9F59D2E734D6}.ServerBuild|x64.Build.0 = Debug|Any CPU + {61157B05-135C-40C5-B694-5F27CE53B334}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61157B05-135C-40C5-B694-5F27CE53B334}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61157B05-135C-40C5-B694-5F27CE53B334}.Debug|x64.ActiveCfg = Debug|Any CPU + {61157B05-135C-40C5-B694-5F27CE53B334}.Debug|x64.Build.0 = Debug|Any CPU + {61157B05-135C-40C5-B694-5F27CE53B334}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61157B05-135C-40C5-B694-5F27CE53B334}.Release|Any CPU.Build.0 = Release|Any CPU + {61157B05-135C-40C5-B694-5F27CE53B334}.Release|x64.ActiveCfg = Release|Any CPU + {61157B05-135C-40C5-B694-5F27CE53B334}.Release|x64.Build.0 = Release|Any CPU + {61157B05-135C-40C5-B694-5F27CE53B334}.ServerBuild|Any CPU.ActiveCfg = Debug|Any CPU + {61157B05-135C-40C5-B694-5F27CE53B334}.ServerBuild|Any CPU.Build.0 = Debug|Any CPU + {61157B05-135C-40C5-B694-5F27CE53B334}.ServerBuild|x64.ActiveCfg = Debug|Any CPU + {61157B05-135C-40C5-B694-5F27CE53B334}.ServerBuild|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE