added auth with a simple UI
This commit is contained in:
10
api/OED.Api/Core/Interfaces/Services/ISessionService.cs
Normal file
10
api/OED.Api/Core/Interfaces/Services/ISessionService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using OED.Api.Core.Models.Eve;
|
||||
|
||||
namespace OED.Api.Core.Interfaces.Services;
|
||||
|
||||
public interface ISessionService
|
||||
{
|
||||
Task<string> CreateAsync(EveSession session);
|
||||
Task<EveSession?> GetAsync(string sessionId);
|
||||
Task DeleteAsync(string sessionId);
|
||||
}
|
||||
9
api/OED.Api/Core/Interfaces/Services/ITokenStore.cs
Normal file
9
api/OED.Api/Core/Interfaces/Services/ITokenStore.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace OED.Api.Core.Interfaces.Services;
|
||||
|
||||
public interface ITokenStore
|
||||
{
|
||||
Task SetAccessTokenAsync(long characterId, string token, TimeSpan expiry);
|
||||
Task<string?> GetAccessTokenAsync(long characterId);
|
||||
Task SetRefreshTokenAsync(long characterId, string token);
|
||||
Task<string?> GetRefreshTokenAsync(long characterId);
|
||||
}
|
||||
11
api/OED.Api/Core/Models/Eve/EveCharacter.cs
Normal file
11
api/OED.Api/Core/Models/Eve/EveCharacter.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace OED.Api.Core.Models.Eve;
|
||||
|
||||
public record EveCharacter
|
||||
{
|
||||
public long CharacterId { get; init; }
|
||||
public string CharacterName { get; init; } = string.Empty;
|
||||
public string[] Scopes { get; init; } = [];
|
||||
public string AccessToken { get; init; } = string.Empty;
|
||||
public string RefreshToken { get; init; } = string.Empty;
|
||||
public DateTimeOffset AccessTokenExpiry { get; init; }
|
||||
}
|
||||
8
api/OED.Api/Core/Models/Eve/EveSession.cs
Normal file
8
api/OED.Api/Core/Models/Eve/EveSession.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace OED.Api.Core.Models.Eve;
|
||||
|
||||
public record EveSession
|
||||
{
|
||||
public long CharacterId { get; init; }
|
||||
public string CharacterName { get; init; } = string.Empty;
|
||||
public string[] Scopes { get; init; } = [];
|
||||
}
|
||||
148
api/OED.Api/Endpoints/AuthEndpoints.cs
Normal file
148
api/OED.Api/Endpoints/AuthEndpoints.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using OED.Api.Core.Interfaces.Services;
|
||||
using OED.Api.Core.Models.Eve;
|
||||
using OED.Api.Infrastructure.Auth;
|
||||
using OED.Api.Infrastructure.Esi;
|
||||
|
||||
namespace OED.Api.Endpoints;
|
||||
|
||||
public static class AuthEndpoints
|
||||
{
|
||||
public static void MapAuthEndpoints(this WebApplication app)
|
||||
{
|
||||
app.MapGet("/auth/login", Login);
|
||||
app.MapGet("/auth/callback", Callback);
|
||||
app.MapGet("/auth/me", Me);
|
||||
app.MapPost("/auth/logout", Logout);
|
||||
}
|
||||
|
||||
private static async Task<IResult> Login(IConfiguration config, IStateStore stateStore)
|
||||
{
|
||||
var clientId = config["Eve:ClientId"]!;
|
||||
var redirectUri = Uri.EscapeDataString(config["Eve:CallbackUrl"]!);
|
||||
var scopes = Uri.EscapeDataString("esi-markets.structure_markets.v1");
|
||||
var state = await stateStore.GenerateAsync();
|
||||
|
||||
var url = "https://login.eveonline.com/v2/oauth/authorize" +
|
||||
$"?response_type=code" +
|
||||
$"&redirect_uri={redirectUri}" +
|
||||
$"&client_id={clientId}" +
|
||||
$"&scope={scopes}" +
|
||||
$"&state={state}";
|
||||
|
||||
return Results.Redirect(url);
|
||||
}
|
||||
|
||||
private static async Task<IResult> Callback(
|
||||
HttpContext context,
|
||||
string code,
|
||||
string state,
|
||||
IConfiguration config,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
EveJwtValidator jwtValidator,
|
||||
ISessionService sessionService,
|
||||
ITokenStore tokenStore,
|
||||
IStateStore stateStore,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger("AuthEndpoints");
|
||||
logger.LogInformation("Callback hit. Code: {Code}, State: {State}", code, state);
|
||||
|
||||
var frontendUrl = config["Frontend:Url"]!;
|
||||
|
||||
// Validate and immediately consume the state — prevents replay
|
||||
if (!await stateStore.ValidateAndConsumeAsync(state))
|
||||
{
|
||||
logger.LogWarning("State validation failed for state: {State}", state);
|
||||
return Results.Redirect($"{frontendUrl}/?error=invalid_state");
|
||||
}
|
||||
|
||||
var clientId = config["Eve:ClientId"]!;
|
||||
var secretKey = config["Eve:SecretKey"]!;
|
||||
|
||||
var credentials = Convert.ToBase64String(
|
||||
System.Text.Encoding.UTF8.GetBytes($"{clientId}:{secretKey}")
|
||||
);
|
||||
|
||||
var client = httpClientFactory.CreateClient();
|
||||
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, "https://login.eveonline.com/v2/oauth/token");
|
||||
tokenRequest.Headers.Add("Authorization", $"Basic {credentials}");
|
||||
tokenRequest.Content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
["grant_type"] = "authorization_code",
|
||||
["code"] = code
|
||||
});
|
||||
|
||||
var tokenResponse = await client.SendAsync(tokenRequest);
|
||||
if (!tokenResponse.IsSuccessStatusCode)
|
||||
return Results.Redirect($"{frontendUrl}/?error=token_exchange_failed");
|
||||
|
||||
var tokens = await tokenResponse.Content.ReadFromJsonAsync<EsiTokenResponse>()
|
||||
?? throw new InvalidOperationException("Empty response from EVE SSO");
|
||||
|
||||
// Validate JWT — throws if tampered, expired, or wrong issuer
|
||||
var jwt = await jwtValidator.ValidateAsync(tokens.AccessToken, clientId);
|
||||
|
||||
var sub = jwt.Subject; // "CHARACTER:EVE:123456789"
|
||||
var characterId = long.Parse(sub.Split(':')[2]);
|
||||
var characterName = jwt.Claims.First(c => c.Type == "name").Value;
|
||||
var scopes = jwt.Claims
|
||||
.Where(c => c.Type == "scp")
|
||||
.Select(c => c.Value)
|
||||
.ToArray();
|
||||
|
||||
await tokenStore.SetAccessTokenAsync(
|
||||
characterId,
|
||||
tokens.AccessToken,
|
||||
TimeSpan.FromSeconds(tokens.ExpiresIn - 30)
|
||||
);
|
||||
await tokenStore.SetRefreshTokenAsync(characterId, tokens.RefreshToken);
|
||||
|
||||
var session = new EveSession
|
||||
{
|
||||
CharacterId = characterId,
|
||||
CharacterName = characterName,
|
||||
Scopes = scopes
|
||||
};
|
||||
|
||||
var sessionId = await sessionService.CreateAsync(session);
|
||||
|
||||
context.Response.Cookies.Append("session", sessionId, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = false,
|
||||
SameSite = SameSiteMode.Lax,
|
||||
Domain = "localhost",
|
||||
Expires = DateTimeOffset.UtcNow.AddDays(7)
|
||||
});
|
||||
|
||||
logger.LogInformation("Session created: {SessionId}, redirecting to {Url}", sessionId, frontendUrl);
|
||||
|
||||
return Results.Redirect($"{frontendUrl}/auth/callback?success=true");
|
||||
}
|
||||
|
||||
private static IResult Me(HttpContext context)
|
||||
{
|
||||
var session = context.Items["Session"] as EveSession;
|
||||
if (session is null) return Results.Unauthorized();
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
session.CharacterId,
|
||||
session.CharacterName,
|
||||
session.Scopes
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<IResult> Logout(
|
||||
HttpContext context,
|
||||
ISessionService sessionService)
|
||||
{
|
||||
if (context.Request.Cookies.TryGetValue("session", out var sessionId))
|
||||
{
|
||||
await sessionService.DeleteAsync(sessionId);
|
||||
context.Response.Cookies.Delete("session");
|
||||
}
|
||||
|
||||
return Results.Ok();
|
||||
}
|
||||
}
|
||||
36
api/OED.Api/Infrastructure/Auth/EveJwtValidator.cs
Normal file
36
api/OED.Api/Infrastructure/Auth/EveJwtValidator.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using Microsoft.IdentityModel.Protocols;
|
||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace OED.Api.Infrastructure.Auth;
|
||||
|
||||
public class EveJwtValidator
|
||||
{
|
||||
private const string MetadataAddress = "https://login.eveonline.com/.well-known/oauth-authorization-server";
|
||||
private readonly ConfigurationManager<OpenIdConnectConfiguration> _configManager = new(
|
||||
MetadataAddress,
|
||||
new OpenIdConnectConfigurationRetriever()
|
||||
);
|
||||
|
||||
public async Task<JwtSecurityToken> ValidateAsync(string token, string clientId)
|
||||
{
|
||||
var config = await _configManager.GetConfigurationAsync();
|
||||
|
||||
var parameters = new TokenValidationParameters
|
||||
{
|
||||
ValidIssuer = "https://login.eveonline.com",
|
||||
ValidAudiences = [clientId, "EVE Online"],
|
||||
IssuerSigningKeys = config.SigningKeys,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidateAudience = true,
|
||||
ValidateIssuer = true,
|
||||
};
|
||||
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
handler.ValidateToken(token, parameters, out var validatedToken);
|
||||
|
||||
return (JwtSecurityToken)validatedToken;
|
||||
}
|
||||
}
|
||||
23
api/OED.Api/Infrastructure/Auth/SessionMiddleware.cs
Normal file
23
api/OED.Api/Infrastructure/Auth/SessionMiddleware.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using OED.Api.Core.Interfaces.Services;
|
||||
|
||||
namespace OED.Api.Infrastructure.Auth;
|
||||
|
||||
// Runs on every request — reads the session cookie and stamps CharacterId onto HttpContext.Items
|
||||
public class SessionMiddleware(RequestDelegate next)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context, ISessionService sessionService)
|
||||
{
|
||||
if (context.Request.Cookies.TryGetValue("session", out var sessionId))
|
||||
{
|
||||
var session = await sessionService.GetAsync(sessionId);
|
||||
if (session is not null)
|
||||
{
|
||||
context.Items["CharacterId"] = session.CharacterId;
|
||||
context.Items["CharacterName"] = session.CharacterName;
|
||||
context.Items["Session"] = session;
|
||||
}
|
||||
}
|
||||
|
||||
await next(context);
|
||||
}
|
||||
}
|
||||
32
api/OED.Api/Infrastructure/Auth/SessionService.cs
Normal file
32
api/OED.Api/Infrastructure/Auth/SessionService.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Text.Json;
|
||||
using OED.Api.Core.Interfaces.Services;
|
||||
using OED.Api.Core.Models.Eve;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace OED.Api.Infrastructure.Auth;
|
||||
|
||||
public class SessionService(IConnectionMultiplexer redis) : ISessionService
|
||||
{
|
||||
private readonly IDatabase _db = redis.GetDatabase();
|
||||
private static readonly TimeSpan SessionTtl = TimeSpan.FromDays(7);
|
||||
|
||||
public async Task<string> CreateAsync(EveSession session)
|
||||
{
|
||||
var sessionId = Guid.NewGuid().ToString("N");
|
||||
var json = JsonSerializer.Serialize(session);
|
||||
await _db.StringSetAsync($"session:{sessionId}", json, SessionTtl);
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public async Task<EveSession?> GetAsync(string sessionId)
|
||||
{
|
||||
var json = await _db.StringGetAsync($"session:{sessionId}");
|
||||
if (json.IsNullOrEmpty) return null;
|
||||
return JsonSerializer.Deserialize<EveSession>((string)json!);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string sessionId)
|
||||
{
|
||||
await _db.KeyDeleteAsync($"session:{sessionId}");
|
||||
}
|
||||
}
|
||||
28
api/OED.Api/Infrastructure/Auth/StateStore.cs
Normal file
28
api/OED.Api/Infrastructure/Auth/StateStore.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace OED.Api.Infrastructure.Auth;
|
||||
|
||||
public interface IStateStore
|
||||
{
|
||||
Task<string> GenerateAsync();
|
||||
Task<bool> ValidateAndConsumeAsync(string state);
|
||||
}
|
||||
|
||||
public class StateStore(IConnectionMultiplexer redis) : IStateStore
|
||||
{
|
||||
private readonly IDatabase _db = redis.GetDatabase();
|
||||
private static readonly TimeSpan StateTtl = TimeSpan.FromMinutes(10);
|
||||
|
||||
public async Task<string> GenerateAsync()
|
||||
{
|
||||
var state = Guid.NewGuid().ToString("N");
|
||||
await _db.StringSetAsync($"oauth:state:{state}", "1", StateTtl);
|
||||
return state;
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateAndConsumeAsync(string state)
|
||||
{
|
||||
// Delete returns true only if the key existed — atomic check + delete in one operation
|
||||
return await _db.KeyDeleteAsync($"oauth:state:{state}");
|
||||
}
|
||||
}
|
||||
18
api/OED.Api/Infrastructure/Auth/TokenEcryptor.cs
Normal file
18
api/OED.Api/Infrastructure/Auth/TokenEcryptor.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
namespace OED.Api.Infrastructure.Auth;
|
||||
|
||||
public interface ITokenEncryptor
|
||||
{
|
||||
string Encrypt(string plaintext);
|
||||
string Decrypt(string ciphertext);
|
||||
}
|
||||
|
||||
// Uses ASP.NET Data Protection — handles key management, rotation, and storage automatically
|
||||
public class TokenEncryptor(IDataProtectionProvider provider) : ITokenEncryptor
|
||||
{
|
||||
private readonly IDataProtector _protector = provider.CreateProtector("Eve.RefreshToken.v1");
|
||||
|
||||
public string Encrypt(string plaintext) => _protector.Protect(plaintext);
|
||||
public string Decrypt(string ciphertext) => _protector.Unprotect(ciphertext);
|
||||
}
|
||||
31
api/OED.Api/Infrastructure/Auth/TokenStore.cs
Normal file
31
api/OED.Api/Infrastructure/Auth/TokenStore.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using OED.Api.Core.Interfaces.Services;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace OED.Api.Infrastructure.Auth;
|
||||
|
||||
public class TokenStore(IConnectionMultiplexer redis, ITokenEncryptor encryptor) : ITokenStore
|
||||
{
|
||||
private readonly IDatabase _db = redis.GetDatabase();
|
||||
|
||||
public async Task SetAccessTokenAsync(long characterId, string token, TimeSpan expiry)
|
||||
=> await _db.StringSetAsync($"accesstoken:{characterId}", token, expiry);
|
||||
|
||||
public async Task<string?> GetAccessTokenAsync(long characterId)
|
||||
{
|
||||
var val = await _db.StringGetAsync($"accesstoken:{characterId}");
|
||||
return val.IsNullOrEmpty ? null : val.ToString();
|
||||
}
|
||||
|
||||
public async Task SetRefreshTokenAsync(long characterId, string token)
|
||||
{
|
||||
var encrypted = encryptor.Encrypt(token);
|
||||
await _db.StringSetAsync($"refreshtoken:{characterId}", encrypted);
|
||||
}
|
||||
|
||||
public async Task<string?> GetRefreshTokenAsync(long characterId)
|
||||
{
|
||||
var val = await _db.StringGetAsync($"refreshtoken:{characterId}");
|
||||
if (val.IsNullOrEmpty) return null;
|
||||
return encryptor.Decrypt(val.ToString());
|
||||
}
|
||||
}
|
||||
36
api/OED.Api/Infrastructure/Esi/EsiAuthHandler.cs
Normal file
36
api/OED.Api/Infrastructure/Esi/EsiAuthHandler.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Net.Http.Headers;
|
||||
using OED.Api.Core.Interfaces.Services;
|
||||
|
||||
namespace OED.Api.Infrastructure.Esi;
|
||||
|
||||
public class EsiAuthHandler(
|
||||
ITokenStore tokenStore,
|
||||
IEsiTokenRefreshService refreshService,
|
||||
IHttpContextAccessor httpContextAccessor
|
||||
) : DelegatingHandler
|
||||
{
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var characterId = GetCharacterIdFromSession();
|
||||
if (characterId is null)
|
||||
return await base.SendAsync(request, cancellationToken);
|
||||
|
||||
var accessToken = await tokenStore.GetAccessTokenAsync(characterId.Value);
|
||||
|
||||
if (accessToken is null)
|
||||
accessToken = await refreshService.RefreshAsync(characterId.Value);
|
||||
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
return await base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
private long? GetCharacterIdFromSession()
|
||||
{
|
||||
var context = httpContextAccessor.HttpContext;
|
||||
// CharacterId is set on the HttpContext by session middleware
|
||||
return context?.Items["CharacterId"] as long?;
|
||||
}
|
||||
}
|
||||
61
api/OED.Api/Infrastructure/Esi/EsiTokenRefreshService.cs
Normal file
61
api/OED.Api/Infrastructure/Esi/EsiTokenRefreshService.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using OED.Api.Core.Interfaces.Services;
|
||||
|
||||
namespace OED.Api.Infrastructure.Esi;
|
||||
|
||||
public interface IEsiTokenRefreshService
|
||||
{
|
||||
Task<string> RefreshAsync(long characterId);
|
||||
}
|
||||
|
||||
public class EsiTokenRefreshService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ITokenStore tokenStore,
|
||||
IConfiguration config
|
||||
) : IEsiTokenRefreshService
|
||||
{
|
||||
public async Task<string> RefreshAsync(long characterId)
|
||||
{
|
||||
var refreshToken = await tokenStore.GetRefreshTokenAsync(characterId)
|
||||
?? throw new InvalidOperationException($"No refresh token found for character {characterId}");
|
||||
|
||||
var clientId = config["Eve:ClientId"]!;
|
||||
var secretKey = config["Eve:SecretKey"]!;
|
||||
|
||||
var credentials = Convert.ToBase64String(
|
||||
System.Text.Encoding.UTF8.GetBytes($"{clientId}:{secretKey}")
|
||||
);
|
||||
|
||||
var client = httpClientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "https://login.eveonline.com/v2/oauth/token");
|
||||
request.Headers.Add("Authorization", $"Basic {credentials}");
|
||||
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
["grant_type"] = "refresh_token",
|
||||
["refresh_token"] = refreshToken
|
||||
});
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<EsiTokenResponse>()
|
||||
?? throw new InvalidOperationException("Empty token response from EVE SSO");
|
||||
|
||||
// Always overwrite — EVE may rotate the refresh token
|
||||
await tokenStore.SetRefreshTokenAsync(characterId, result.RefreshToken);
|
||||
await tokenStore.SetAccessTokenAsync(characterId, result.AccessToken, TimeSpan.FromSeconds(result.ExpiresIn - 30));
|
||||
|
||||
return result.AccessToken;
|
||||
}
|
||||
}
|
||||
|
||||
public record EsiTokenResponse(
|
||||
string access_token,
|
||||
int expires_in,
|
||||
string token_type,
|
||||
string refresh_token
|
||||
)
|
||||
{
|
||||
public string AccessToken => access_token;
|
||||
public int ExpiresIn => expires_in;
|
||||
public string RefreshToken => refresh_token;
|
||||
}
|
||||
@@ -7,7 +7,26 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3"/>
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.23" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.21.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.16.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="Refit" Version="10.0.1" />
|
||||
<PackageReference Include="Refit.HttpClientFactory" Version="10.0.1" />
|
||||
<PackageReference Include="Riok.Mapperly" Version="4.3.1" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.13.2" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.11.8" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
@OED.Api_HostAddress = http://localhost:5163
|
||||
|
||||
GET {{OED.Api_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -1,21 +1,51 @@
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using OED.Api.Core.Interfaces.Services;
|
||||
using OED.Api.Endpoints;
|
||||
using OED.Api.Infrastructure.Auth;
|
||||
using OED.Api.Infrastructure.Esi;
|
||||
using StackExchange.Redis;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddAuthorization();
|
||||
// Redis
|
||||
builder.Services.AddSingleton<IConnectionMultiplexer>(
|
||||
ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis")!)
|
||||
);
|
||||
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddOpenApi();
|
||||
// Data Protection — handles encryption key management automatically
|
||||
// Keys are stored in the local file system by default in dev
|
||||
// In production point this at a shared volume or Azure Key Vault
|
||||
builder.Services.AddDataProtection()
|
||||
.SetApplicationName("OED.Api");
|
||||
|
||||
// Auth & session
|
||||
builder.Services.AddSingleton<EveJwtValidator>();
|
||||
builder.Services.AddScoped<ISessionService, SessionService>();
|
||||
builder.Services.AddScoped<ITokenStore, TokenStore>();
|
||||
builder.Services.AddScoped<IStateStore, StateStore>();
|
||||
builder.Services.AddScoped<ITokenEncryptor, TokenEncryptor>();
|
||||
builder.Services.AddScoped<IEsiTokenRefreshService, EsiTokenRefreshService>();
|
||||
builder.Services.AddTransient<EsiAuthHandler>();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
// CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("Frontend", policy =>
|
||||
policy
|
||||
.WithOrigins(builder.Configuration["Frontend:Url"]!)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials()
|
||||
);
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
app.UseCors("Frontend");
|
||||
app.UseMiddleware<SessionMiddleware>();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.MapAuthEndpoints();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.Run();
|
||||
app.Run();
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
"Eve": {
|
||||
"ClientId": "0a2e13f0b0f3426ca8f09af5036978a9",
|
||||
"SecretKey": "eat_vFwYVybPjD1QkPZM4fXJG2AkrzJO9W8U_10Pqp9",
|
||||
"CallbackUrl": "http://localhost:5163/auth/callback"
|
||||
},
|
||||
"Frontend": {
|
||||
"Url": "http://localhost:5173"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"Redis": "localhost:6379,password=devpassword",
|
||||
"Postgres": "Host=localhost;Port=5432;Database=oed;Username=oed;Password=devpassword"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user