From 2bdb51b0a9e176d91e147658d3ad5619508aa0c2 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 18 May 2024 12:54:58 +0200 Subject: [PATCH] azure storage integration + stripe integration --- MicrobotApi/ConnectionManager.cs | 6 - MicrobotApi/Controllers/AuthController.cs | 112 +++++++++++++++ .../Controllers/CheckoutApiController.cs | 60 ++++++++ MicrobotApi/Controllers/FileController.cs | 35 +++++ .../Controllers/ScriptKeysController.cs | 48 +++++++ MicrobotApi/Controllers/SessionController.cs | 131 ++++++++++++++++++ MicrobotApi/Controllers/WebHookController.cs | 84 +++++++++++ MicrobotApi/Database/MicrobotContext.cs | 55 ++++++++ MicrobotApi/Extensions/UserExtension.cs | 13 ++ .../Handlers/DiscordAuthenticationHandler.cs | 44 ++++++ MicrobotApi/MicrobotApi.csproj | 11 ++ MicrobotApi/Models/Discord/DiscordUser.cs | 18 +++ MicrobotApi/Models/Discord/OAuthResponse.cs | 14 ++ MicrobotApi/Models/Discord/TokenResponse.cs | 11 ++ MicrobotApi/Program.cs | 95 ++++++++++--- MicrobotApi/Services/AzureStorageService.cs | 54 ++++++++ MicrobotApi/Services/DiscordService.cs | 72 ++++++++++ MicrobotApi/TokenFilter.cs | 13 +- MicrobotApi/TokenRequestModel.cs | 2 +- .../DiscordAuthorizationMiddleware.cs | 34 +++++ 20 files changed, 881 insertions(+), 31 deletions(-) delete mode 100644 MicrobotApi/ConnectionManager.cs create mode 100644 MicrobotApi/Controllers/AuthController.cs create mode 100644 MicrobotApi/Controllers/CheckoutApiController.cs create mode 100644 MicrobotApi/Controllers/FileController.cs create mode 100644 MicrobotApi/Controllers/ScriptKeysController.cs create mode 100644 MicrobotApi/Controllers/SessionController.cs create mode 100644 MicrobotApi/Controllers/WebHookController.cs create mode 100644 MicrobotApi/Database/MicrobotContext.cs create mode 100644 MicrobotApi/Extensions/UserExtension.cs create mode 100644 MicrobotApi/Handlers/DiscordAuthenticationHandler.cs create mode 100644 MicrobotApi/Models/Discord/DiscordUser.cs create mode 100644 MicrobotApi/Models/Discord/OAuthResponse.cs create mode 100644 MicrobotApi/Models/Discord/TokenResponse.cs create mode 100644 MicrobotApi/Services/AzureStorageService.cs create mode 100644 MicrobotApi/Services/DiscordService.cs create mode 100644 MicrobotApi/middleware/DiscordAuthorizationMiddleware.cs diff --git a/MicrobotApi/ConnectionManager.cs b/MicrobotApi/ConnectionManager.cs deleted file mode 100644 index 64fe8a9..0000000 --- a/MicrobotApi/ConnectionManager.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MicrobotApi; - -public class ConnectionManager -{ - public readonly List Connections = []; -} \ No newline at end of file diff --git a/MicrobotApi/Controllers/AuthController.cs b/MicrobotApi/Controllers/AuthController.cs new file mode 100644 index 0000000..1b16c64 --- /dev/null +++ b/MicrobotApi/Controllers/AuthController.cs @@ -0,0 +1,112 @@ +using System.Security.Claims; +using MicrobotApi.Database; +using MicrobotApi.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace MicrobotApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AuthController : Controller +{ + private readonly IConfiguration _configuration; + private readonly DiscordService _discordService; + private readonly MicrobotContext _microbotContext; + + public AuthController(IConfiguration configuration, DiscordService discordService, MicrobotContext microbotContext) + { + _configuration = configuration; + _discordService = discordService; + _microbotContext = microbotContext; + } + + [HttpGet("discord/user")] + public async Task DiscordUserInfo([FromQuery] String code) + { + if (!string.IsNullOrWhiteSpace(code)) + { + var clientId = _configuration["Discord:ClientId"] ?? string.Empty; + var clientSecret = _configuration["Discord:ClientSecret"] ?? string.Empty; + var redirectUri = _configuration["Discord:RedirectUri"] ?? string.Empty; + var tokenResponse = await _discordService.GetToken(clientId, clientSecret, code, redirectUri); + + if (tokenResponse == null) + return BadRequest("Invalid code!"); + + var userInfo = await _discordService.GetUserInfo(tokenResponse.Access_Token); + + if (userInfo == null) + return BadRequest("userinfo is empty"); + + var discordUser = await _microbotContext.DiscordUsers.FirstOrDefaultAsync(x => x.DiscordId == userInfo.Id); + + if (discordUser == null) + { + _microbotContext.Users.Add(new DiscordUser() + { + DiscordId = userInfo.Id, + Token = tokenResponse.Access_Token, + RefreshToken = tokenResponse.Refresh_Token, + TokenExpiry = DateTime.UtcNow.AddSeconds(tokenResponse.Expires_In), + }); + await _microbotContext.SaveChangesAsync(); + } + + return Ok(tokenResponse.Access_Token); + + } + + return BadRequest("Code is missing!"); + } + + [HttpGet("test")] + [Authorize] + public async Task Test() + { + return Ok("hello world"); + } + // [HttpGet("discord/token/{userId}")] + // public async Task Token(string userId = "126659209642246144") + // { + // var discordUser = await _microbotContext.DiscordUsers.FirstOrDefaultAsync(x => x.DiscordId == userId); + // + // if (discordUser == null) + // return BadRequest("User not found"); + // + // if (discordUser.TokenExpiry < DateTime.UtcNow) + // return Ok(discordUser.Token); + // + // var clientId = _configuration["Discord:ClientId"]; + // var clientSecret = _configuration["Discord:ClientSecret"]; + // var redirectUri = _configuration["Discord:RedirectUri"]; + // + // var token = await _discordService.RefreshAccessToken(clientId, clientSecret, discordUser.RefreshToken, redirectUri); + // + // if (string.IsNullOrWhiteSpace(token)) + // return BadRequest("Invalid code!"); + // + // return Ok(token); + // + // } + + [HttpGet("userinfo")] + [Authorize] + public async Task UserInfo() + { + var userId = User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value; + + var discordUser = await _microbotContext.DiscordUsers.FirstOrDefaultAsync(x => x.DiscordId == userId); + + if (discordUser != null) + { + var userInfo = await _discordService.GetUserInfo(discordUser.Token); + + return Ok(userInfo); + } + + + return NotFound("User not found."); + } +} \ No newline at end of file diff --git a/MicrobotApi/Controllers/CheckoutApiController.cs b/MicrobotApi/Controllers/CheckoutApiController.cs new file mode 100644 index 0000000..ca26157 --- /dev/null +++ b/MicrobotApi/Controllers/CheckoutApiController.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Stripe; +using Stripe.Checkout; +using Stripe.Identity; + +namespace MicrobotApi.Controllers; + +[Route("create-checkout-session")] +[ApiController] +public class CheckoutApiController : Controller +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + + public CheckoutApiController(ILogger logger, IConfiguration configuration) + { + _logger = logger; + _configuration = configuration; + } + + [HttpPost] + [Authorize] + public ActionResult Create([FromBody] CreateCheckOutRequest createCheckOutRequest) + { + var domain = _configuration["Discord:RedirectUri"]; + var options = new SessionCreateOptions + { + LineItems = new List + { + new() + { + // Provide the exact Price ID (for example, pr_1234) of the product you want to sell + Price = _configuration["Stripe:PriceSecret"], + Quantity = 1, + }, + }, + Metadata = new Dictionary + { + { "userId", createCheckOutRequest.UserId } // Add user ID as metadata + }, + Mode = "payment", + SuccessUrl = domain + "/success", + CancelUrl = domain + "/cancel", + AutomaticTax = new SessionAutomaticTaxOptions { Enabled = true }, + }; + var service = new SessionService(); + Session session = service.Create(options); + + Response.Headers.Append("Location", session.Url); + + return Ok(session); + } +} + +public class CreateCheckOutRequest +{ + public string UserId { get; set; } +} + diff --git a/MicrobotApi/Controllers/FileController.cs b/MicrobotApi/Controllers/FileController.cs new file mode 100644 index 0000000..48775b2 --- /dev/null +++ b/MicrobotApi/Controllers/FileController.cs @@ -0,0 +1,35 @@ +using MicrobotApi.Database; +using MicrobotApi.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace MicrobotApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class FileController : Controller +{ + private readonly AzureStorageService _azureStorageService; + private readonly MicrobotContext _microbotContext; + + public FileController(AzureStorageService azureStorageService, MicrobotContext microbotContext) + { + _azureStorageService = azureStorageService; + _microbotContext = microbotContext; + } + + [Authorize] + [HttpGet("download/{blobName}/{key}/{hwid}")] + public async Task Download(string blobName, string key, string hwid) + { + var exists = await _microbotContext.Keys.AnyAsync(x => x.Key == key && x.HWID == hwid); + + if (!exists) + return Unauthorized(); + + var file = await _azureStorageService.DownloadFile(blobName); + + return File(file.Value.Content, "application/octet-stream", blobName); + } +} \ No newline at end of file diff --git a/MicrobotApi/Controllers/ScriptKeysController.cs b/MicrobotApi/Controllers/ScriptKeysController.cs new file mode 100644 index 0000000..39dbbd3 --- /dev/null +++ b/MicrobotApi/Controllers/ScriptKeysController.cs @@ -0,0 +1,48 @@ +using MicrobotApi.Database; +using MicrobotApi.Extensions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace MicrobotApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ScriptKeysController(MicrobotContext microbotContext) : Controller +{ + [HttpPost] + [Authorize] + public async Task Create([FromBody] HmacRequest request) + { + var key = await microbotContext.Keys.FirstOrDefaultAsync(x => x.Key == request.Key); + + if (key == null) + return NotFound("Key not found!"); + + key.Active = true; + key.HWID = request.HWID; + + await microbotContext.SaveChangesAsync(); + + return Ok(); + } + + [HttpGet] + [Authorize] + public async Task Get() + { + var keys = await microbotContext.DiscordUsers + .Include(x => x.Keys) + .Where(x => x.DiscordId == User.GetUserId()) + .Select(x => x.Keys) + .FirstOrDefaultAsync(); + + return Ok(keys); + } + + public class HmacRequest + { + public string Key { get; set; } + public string HWID { get; set; } + } +} \ No newline at end of file diff --git a/MicrobotApi/Controllers/SessionController.cs b/MicrobotApi/Controllers/SessionController.cs new file mode 100644 index 0000000..c90e626 --- /dev/null +++ b/MicrobotApi/Controllers/SessionController.cs @@ -0,0 +1,131 @@ +using MicrobotApi.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; + +namespace MicrobotApi.Controllers; + +using Microsoft.AspNetCore.Mvc; + +[ApiController] +[Route("api/[controller]")] +public class SessionController : Controller +{ + private readonly MicrobotContext _microbotContext; + private readonly IMemoryCache _memoryCache; + private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5); + + public SessionController(MicrobotContext microbotContext, IMemoryCache memoryCache) + { + _microbotContext = microbotContext; + _memoryCache = memoryCache; + } + + [HttpGet("")] + public async Task Session([FromQuery] Guid? sessionId, [FromQuery] bool? isLoggedIn, [FromQuery] string? version) + { + if (sessionId.HasValue && isLoggedIn.HasValue && version != null) + { + var result = await _microbotContext.Sessions.FirstOrDefaultAsync(x => x.Id == sessionId); + if (result != null) + { + result.IsLoggedIn = isLoggedIn.Value; + result.Version = version; + result.LastPing = DateTime.UtcNow; + await _microbotContext.SaveChangesAsync(); + } + return Ok(sessionId); + } + + var session = new Session() + { + IsLoggedIn = false, + Version = "", + LastPing = DateTime.UtcNow + }; + + _microbotContext.Sessions.Add(session); + + await _microbotContext.SaveChangesAsync(); + + return Ok(session.Id); + } + + [HttpDelete("")] + public async Task DeleteSession([FromQuery] Guid sessionId) + { + var session = await _microbotContext.Sessions.FirstOrDefaultAsync(x => x.Id == sessionId); + + if (session == null) return BadRequest("Session id not found."); + + _microbotContext.Sessions.Remove(session); + await _microbotContext.SaveChangesAsync(); + + return Ok(); + } + + [HttpGet("count")] + public IActionResult Count() + { + return Ok(GetCachedCount()); + } + + [HttpGet("count/loggedIn")] + public IActionResult CountLoggedIn() + { + return Ok(GetCachedLoggedInCount()); + } + + private int GetCachedCount() + { + if (!_memoryCache.TryGetValue("CachedCount", out int cachedData)) + { + // Data not in cache, so get it from the source + cachedData = GetCount(); + + // Set cache options + var cacheEntryOptions = new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = _cacheDuration, + SlidingExpiration = _cacheDuration + }; + + // Save data in cache + _memoryCache.Set("CachedCount", cachedData, cacheEntryOptions); + } + + return cachedData; + } + + private int GetCachedLoggedInCount() + { + if (!_memoryCache.TryGetValue("CachedLoggedInCount", out int cachedData)) + { + // Data not in cache, so get it from the source + cachedData = GetCountLoggedIn(); + + // Set cache options + var cacheEntryOptions = new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = _cacheDuration, + SlidingExpiration = _cacheDuration + }; + + // Save data in cache + _memoryCache.Set("CachedLoggedInCount", cachedData, cacheEntryOptions); + } + + return cachedData; + } + + private int GetCountLoggedIn() + { + var count = _microbotContext.Sessions.Count(x => x.IsLoggedIn && x.LastPing > DateTime.UtcNow.AddMinutes(-5)); + return count; + } + + private int GetCount() + { + var count = _microbotContext.Sessions.Count(x => x.IsLoggedIn && x.LastPing > DateTime.UtcNow.AddMinutes(-5)); + return count; + } +} \ No newline at end of file diff --git a/MicrobotApi/Controllers/WebHookController.cs b/MicrobotApi/Controllers/WebHookController.cs new file mode 100644 index 0000000..20e8ff0 --- /dev/null +++ b/MicrobotApi/Controllers/WebHookController.cs @@ -0,0 +1,84 @@ +using MicrobotApi.Database; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Stripe; +using Session = Stripe.Checkout.Session; + +namespace MicrobotApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class WebHookController(ILogger logger, MicrobotContext microbotContext, IConfiguration configuration) + : Controller +{ + [HttpPost] + public async Task Webhook() + { + var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); + var endpointSecret = configuration["Stripe:PriceSecret"]; // Set this to your Stripe webhook secret + + try + { + var stripeEvent = EventUtility.ConstructEvent( + json, + Request.Headers["Stripe-Signature"], + endpointSecret + ); + + // Handle the event + if (stripeEvent.Type == Events.CheckoutSessionCompleted) + { + var session = stripeEvent.Data.Object as Session; + // Handle the successful checkout session + await HandleCheckoutSession(session); + } + else + { + // Handle other event types + logger.LogInformation($"Unhandled event type: {stripeEvent.Type}"); + } + + return Ok(); + } + catch (StripeException e) + { + logger.LogError(e, "Stripe webhook error"); + return BadRequest(); + } + } + + private async Task HandleCheckoutSession(Session? session) + { + // Implement your business logic for handling a successful checkout session + logger.LogInformation($"Payment for session {session?.Id} was successful."); + // You can use session.PaymentIntentId to retrieve more details about the payment if needed + var userId = session?.Metadata["userId"]; + + if (userId == null) + { + logger.LogWarning("User id is null in checkout"); + return; + } + + var user = await microbotContext.DiscordUsers.FirstOrDefaultAsync(x => x.DiscordId == userId); + + if (user == null) + { + logger.LogWarning("User not found"); + return; + } + + var key = new ScriptKey() + { + Key = Guid.NewGuid().ToString(), + Active = true + }; + + + microbotContext.Keys.Add(key); + + user.Keys.Add(key); + + await microbotContext.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/MicrobotApi/Database/MicrobotContext.cs b/MicrobotApi/Database/MicrobotContext.cs new file mode 100644 index 0000000..ca5ff30 --- /dev/null +++ b/MicrobotApi/Database/MicrobotContext.cs @@ -0,0 +1,55 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; + +namespace MicrobotApi.Database; + +public class MicrobotContext : DbContext +{ + public MicrobotContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Sessions { get; set; } + public DbSet Users { get; set; } + public DbSet DiscordUsers { get; set; } + public DbSet Keys { get; set; } + +} + +[Index(nameof(DiscordId))] +public class DiscordUser : User +{ + [MaxLength(255)] + public string DiscordId { get; set; } + + public List Keys { get; set; } +} + +public class User +{ + public Guid Id { get; set; } + public string Token { get; set; } + public string RefreshToken { get; set; } + public DateTime TokenExpiry { get; set; } + public bool IsPremium { get; set; } + public string Type { get; set; } + +} + +public class Session +{ + public Guid Id { get; set; } + public bool IsLoggedIn { get; set; } + + public string Version { get; set; } + public DateTime LastPing { get; set; } +} + +public class ScriptKey +{ + public Guid Id { get; set; } + public string Key { get; set; } + public string HWID { get; set; } + public bool Active { get; set; } +} \ No newline at end of file diff --git a/MicrobotApi/Extensions/UserExtension.cs b/MicrobotApi/Extensions/UserExtension.cs new file mode 100644 index 0000000..83d8fc5 --- /dev/null +++ b/MicrobotApi/Extensions/UserExtension.cs @@ -0,0 +1,13 @@ +using System.Security.Claims; + +namespace MicrobotApi.Extensions; + +public static class UserExtension +{ + public static string? GetUserId(this ClaimsPrincipal user) + { + var userId = user.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value; + + return userId; + } +} \ No newline at end of file diff --git a/MicrobotApi/Handlers/DiscordAuthenticationHandler.cs b/MicrobotApi/Handlers/DiscordAuthenticationHandler.cs new file mode 100644 index 0000000..05ab473 --- /dev/null +++ b/MicrobotApi/Handlers/DiscordAuthenticationHandler.cs @@ -0,0 +1,44 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; +using MicrobotApi.Services; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; + +namespace MicrobotApi.Handlers; + +public class DiscordAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + DiscordService discordService) + : AuthenticationHandler(options, logger, encoder) +{ + protected override async Task HandleAuthenticateAsync() + { + // Extract your credentials from the request + var token = Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); + if (string.IsNullOrEmpty(token)) + { + return AuthenticateResult.Fail("No token provided."); + } + + // Validate the token by making a request to Discord's API or using your method + var userInfo = await discordService.GetUserInfo(token); + if (userInfo != null) + { + var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userInfo.Id) }; + var identity = new ClaimsIdentity(claims, Scheme.Name); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, Scheme.Name); + return AuthenticateResult.Success(ticket); + } + + + return AuthenticateResult.Fail("Invalid token."); + } +} + +public class CustomAuthenticationOptions : AuthenticationSchemeOptions +{ + // Any custom options here +} \ No newline at end of file diff --git a/MicrobotApi/MicrobotApi.csproj b/MicrobotApi/MicrobotApi.csproj index 23e0b2b..1fceb93 100644 --- a/MicrobotApi/MicrobotApi.csproj +++ b/MicrobotApi/MicrobotApi.csproj @@ -7,8 +7,19 @@ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/MicrobotApi/Models/Discord/DiscordUser.cs b/MicrobotApi/Models/Discord/DiscordUser.cs new file mode 100644 index 0000000..c764086 --- /dev/null +++ b/MicrobotApi/Models/Discord/DiscordUser.cs @@ -0,0 +1,18 @@ +namespace MicrobotApi.Models.Discord; + +public class DiscordUser +{ + public string Id { get; set; } + public string Username { get; set; } + public string Discriminator { get; set; } + public string Avatar { get; set; } + public bool? Bot { get; set; } + public bool? System { get; set; } + public bool MfaEnabled { get; set; } + public string Locale { get; set; } + public bool Verified { get; set; } + public string Email { get; set; } + public int Flags { get; set; } + public int? PremiumType { get; set; } + public int PublicFlags { get; set; } +} \ No newline at end of file diff --git a/MicrobotApi/Models/Discord/OAuthResponse.cs b/MicrobotApi/Models/Discord/OAuthResponse.cs new file mode 100644 index 0000000..617506d --- /dev/null +++ b/MicrobotApi/Models/Discord/OAuthResponse.cs @@ -0,0 +1,14 @@ +namespace MicrobotApi.Models.Discord; + +public class OAuthResponse +{ + public string Access_Token { get; set; } + + public string Token_Type { get; set; } + + public int Expires_In { get; set; } + + public string Refresh_Token { get; set; } + + public string Scope { get; set; } +} diff --git a/MicrobotApi/Models/Discord/TokenResponse.cs b/MicrobotApi/Models/Discord/TokenResponse.cs new file mode 100644 index 0000000..a3a42d3 --- /dev/null +++ b/MicrobotApi/Models/Discord/TokenResponse.cs @@ -0,0 +1,11 @@ +namespace MicrobotApi.Models.Discord; + +public class TokenResponse +{ + public string Token_Type { get; set; } + public string Access_Token { get; set; } + public int Expires_In { get; set; } + public string Refresh_Token { get; set; } + public string Scope { get; set; } + public string Id_Token { get; set; } +} diff --git a/MicrobotApi/Program.cs b/MicrobotApi/Program.cs index 3422723..c729a9a 100644 --- a/MicrobotApi/Program.cs +++ b/MicrobotApi/Program.cs @@ -1,6 +1,15 @@ +using Azure.Storage.Blobs; using MicrobotApi; +using MicrobotApi.Database; +using MicrobotApi.Handlers; +using MicrobotApi.Services; +using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.OpenApi.Models; +using Stripe; var builder = WebApplication.CreateBuilder(args); @@ -18,11 +27,47 @@ }); }); +StripeConfiguration.ApiKey = "sk_test_51PHBkLJ45WMcMRTutHNGpJZvzToGIf0EazZTuT38eTwoRbHuAoqajpCUZ1bdmWperK6jazc7wLdHHdX7x0PFdo6R00vEff23me"; // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Microbot Api", Version = "v1" }); + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Please insert JWT with Bearer into field", + Name = "Authorization", + Type = SecuritySchemeType.ApiKey + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] { } + } + }); +}); + +builder.Services.AddAuthentication(options => + { + // Set the default authentication scheme + options.DefaultAuthenticateScheme = "DiscordScheme"; + options.DefaultChallengeScheme = "DiscordScheme"; + }) + .AddScheme("DiscordScheme", options => {}); + + builder.Services .AddSignalR() .AddHubOptions(options => @@ -30,11 +75,40 @@ // Local filters will run second options.AddFilter(); }); +builder.Services.AddHttpClient(client => +{ + client.BaseAddress = new Uri(builder.Configuration["Discord:Api"]); +}); +builder.Services.AddScoped(); +builder.Services.AddSingleton(c => + new BlobServiceClient(builder.Configuration.GetConnectionString("AzureBlobConnection")) +); +builder.Services.AddDbContext(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("MicrobotContext"))); +builder.Services.AddMemoryCache(); +builder.Services.AddControllers(); + +builder.Services.AddDatabaseDeveloperPageExceptionFilter(); -builder.Services.AddSingleton(); var app = builder.Build(); +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); + app.UseHsts(); +} +else +{ + app.UseDeveloperExceptionPage(); + app.UseMigrationsEndPoint(); +} + + +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { @@ -44,23 +118,8 @@ app.UseHttpsRedirection(); -app.MapGet("/token", () => - { - var token = Guid.NewGuid(); - var connectionManager = app.Services.GetService(); - connectionManager?.Connections.Add(token.ToString()); - return token; - }) - .WithName("getToken") - .WithOpenApi(); -app.MapPost("/token", async ([FromBody] TokenRequestModel tokenRequestModel) => - { - var connectionManager = app.Services.GetService(); - return connectionManager?.Connections.Contains(tokenRequestModel.Token); - }) - .WithName("Check Token Validity") - .WithOpenApi(); app.UseCors(); + app.MapHub("/microbot"); app.Run(); \ No newline at end of file diff --git a/MicrobotApi/Services/AzureStorageService.cs b/MicrobotApi/Services/AzureStorageService.cs new file mode 100644 index 0000000..47a121a --- /dev/null +++ b/MicrobotApi/Services/AzureStorageService.cs @@ -0,0 +1,54 @@ +using Azure; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Azure.Storage.Blobs.Specialized; +using Azure.Storage.Sas; + +namespace MicrobotApi.Services; + +public class AzureStorageService +{ + private readonly BlobServiceClient _blobServiceClient; + private const string BlobContainer = "microbot"; + + public AzureStorageService(BlobServiceClient blobServiceClient) + { + _blobServiceClient = blobServiceClient; + } + + public static Uri GetSasUri(BlobBaseClient blobClient, string storedPolicyName = null) + { + if (!blobClient.CanGenerateSasUri) + return null; + + var sasBuilder = new BlobSasBuilder + { + BlobContainerName = blobClient.GetParentBlobContainerClient().Name, + BlobName = blobClient.Name, + Resource = "b" + }; + + if (storedPolicyName == null) + { + sasBuilder.ExpiresOn = DateTimeOffset.UtcNow.AddHours(2); + sasBuilder.SetPermissions(BlobSasPermissions.Read); + } + else + { + sasBuilder.Identifier = storedPolicyName; + } + + var sasUri = blobClient.GenerateSasUri(sasBuilder); + return sasUri; + } + + public Task> DownloadFile(string storagePath) + { + var containerClient = _blobServiceClient.GetBlobContainerClient(BlobContainer); + var blobClient = containerClient.GetBlobClient(storagePath); + + var blobData = blobClient.DownloadAsync(); + + return blobData; + } +} \ No newline at end of file diff --git a/MicrobotApi/Services/DiscordService.cs b/MicrobotApi/Services/DiscordService.cs new file mode 100644 index 0000000..7f42f83 --- /dev/null +++ b/MicrobotApi/Services/DiscordService.cs @@ -0,0 +1,72 @@ +using System.Text; +using System.Text.Json; +using MicrobotApi.Models.Discord; + +namespace MicrobotApi.Services; + +public class DiscordService(HttpClient httpClient) +{ + + public async Task RefreshAccessToken(string? clientId, string? clientSecret, string refreshToken, string redirectUri) + { + if (string.IsNullOrWhiteSpace(clientId)) + throw new Exception("Could not refresh token because clientid is empty"); + if ( string.IsNullOrWhiteSpace(clientSecret)) + throw new Exception("Could not refresh token because clientsecret is empty"); + + var requestContent = new FormUrlEncodedContent(new[] + { + new KeyValuePair("client_id", clientId), + new KeyValuePair("client_secret", clientSecret), + new KeyValuePair("grant_type", "refresh_token"), + new KeyValuePair("refresh_token", refreshToken), + new KeyValuePair("redirect_uri", redirectUri) + }); + + var response = await httpClient.PostAsync("https://discord.com/api/oauth2/token", requestContent); + var jsonResponse = await response.Content.ReadFromJsonAsync(); + + if (response.IsSuccessStatusCode) + { + return jsonResponse.Access_Token; + } + + // Handle error response + throw new Exception("Could not refresh token: " + jsonResponse); + } + + + public async Task GetToken(string clientId, string clientSecret, string code, string redirectUri) + { + var values = new Dictionary + { + { "client_id", clientId }, + { "client_secret", clientSecret }, + { "grant_type", "authorization_code" }, + { "code", code }, + { "redirect_uri", redirectUri } + }; + + var content = new FormUrlEncodedContent(values); + + var response = await httpClient.PostAsync("oauth2/token", content); + + if (response.IsSuccessStatusCode) + { + var token = await response.Content.ReadFromJsonAsync(); + + if (token?.Access_Token == null) return null; + + return token; + } + return null; + } + + public async Task GetUserInfo(string accessToken) + { + httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + var response = await httpClient.GetAsync("users/@me"); + var userInfo = await response.Content.ReadFromJsonAsync(); + return userInfo; + } +} diff --git a/MicrobotApi/TokenFilter.cs b/MicrobotApi/TokenFilter.cs index 31c9100..e9a6340 100644 --- a/MicrobotApi/TokenFilter.cs +++ b/MicrobotApi/TokenFilter.cs @@ -1,23 +1,24 @@ -using Microsoft.AspNetCore.SignalR; +using MicrobotApi.Database; +using Microsoft.AspNetCore.SignalR; namespace MicrobotApi; public class TokenFilter : IHubFilter { - private readonly ConnectionManager _connectionManager; + private readonly MicrobotContext _microbotContext; private const string invalidTokenMessage = "Invalid token!"; - public TokenFilter(ConnectionManager connectionManager) + public TokenFilter(MicrobotContext microbotContext) { - _connectionManager = connectionManager; + _microbotContext = microbotContext; } public async ValueTask InvokeMethodAsync( HubInvocationContext invocationContext, Func> next) { var token = GetToken(invocationContext.Context); - - var exists = _connectionManager.Connections.Contains(token); + + var exists = !string.IsNullOrWhiteSpace(token) && _microbotContext.Sessions.Any(x => x.Id == new Guid(token)); if (!exists) throw new UnauthorizedAccessException(invalidTokenMessage); diff --git a/MicrobotApi/TokenRequestModel.cs b/MicrobotApi/TokenRequestModel.cs index 3b8a768..c659bd3 100644 --- a/MicrobotApi/TokenRequestModel.cs +++ b/MicrobotApi/TokenRequestModel.cs @@ -2,5 +2,5 @@ public class TokenRequestModel { - public String Token { get; set; } + public string SessionId { get; set; } } \ No newline at end of file diff --git a/MicrobotApi/middleware/DiscordAuthorizationMiddleware.cs b/MicrobotApi/middleware/DiscordAuthorizationMiddleware.cs new file mode 100644 index 0000000..2df9138 --- /dev/null +++ b/MicrobotApi/middleware/DiscordAuthorizationMiddleware.cs @@ -0,0 +1,34 @@ +using MicrobotApi.Services; + +namespace MicrobotApi.middleware; + +public class DiscordAuthorizationMiddleware +{ + private readonly RequestDelegate _next; + private readonly DiscordService _discordService; + + public DiscordAuthorizationMiddleware(RequestDelegate next, DiscordService discordService) + { + _next = next; + _discordService = discordService; + } + + public async Task InvokeAsync(HttpContext context) + { + var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); + if (token == null || !await ValidateToken(token)) + { + context.Response.StatusCode = 401; // Unauthorized + return; + } + + await _next(context); + } + + private async Task ValidateToken(string token) + { + // Validate the token by making a request to Discord's API or using your method + var userInfo = await _discordService.GetUserInfo(token); + return userInfo != null; + } +} \ No newline at end of file