Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions GenHub/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<PackageVersion Include="DotNetEnv" Version="3.1.1" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="CsvHelper" Version="33.1.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.72" />
<!-- Test packages -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="System.Security.Cryptography.ProtectedData" Version="9.0.11" />
Expand Down
106 changes: 106 additions & 0 deletions GenHub/GenHub.Core/Constants/GameReplaysConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
namespace GenHub.Core.Constants;

/// <summary>
/// Constants for GameReplays integration.
/// </summary>
public static class GameReplaysConstants
{
/// <summary>
/// Base URL for GameReplays website.
/// </summary>
public const string BaseUrl = "https://www.gamereplays.org";

/// <summary>
/// Tournament board topic ID.
/// </summary>
public const string TournamentBoardTopicId = "1066376";

/// <summary>
/// OAuth 2.0 authorization endpoint.
/// </summary>
public const string OAuthAuthorizationEndpoint = "https://www.gamereplays.org/oauth/authorize";

/// <summary>
/// OAuth 2.0 token endpoint.
/// </summary>
public const string OAuthTokenEndpoint = "https://www.gamereplays.org/oauth/token";

/// <summary>
/// OAuth 2.0 resource endpoint.
/// </summary>
public const string OAuthResourceEndpoint = "https://www.gamereplays.org/oauth/resource";

/// <summary>
/// OAuth 2.0 client ID.
/// TODO: Replace with actual client ID from GameReplays.
/// </summary>
public const string OAuthClientId = "genhub_client";

/// <summary>
/// OAuth 2.0 client secret.
/// TODO: Replace with actual client secret from GameReplays.
/// </summary>
public const string OAuthClientSecret = "genhub_secret";
Comment on lines +37 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hardcoded OAuth credentials (genhub_client and genhub_secret) are a security risk - these placeholder values should be replaced with actual credentials from environment variables or secure configuration before this code reaches production

Prompt To Fix With AI
This is a comment left during a code review.
Path: GenHub/GenHub.Core/Constants/GameReplaysConstants.cs
Line: 37:43

Comment:
hardcoded OAuth credentials (`genhub_client` and `genhub_secret`) are a security risk - these placeholder values should be replaced with actual credentials from environment variables or secure configuration before this code reaches production

How can I resolve this? If you propose a fix, please make it concise.


/// <summary>
/// OAuth 2.0 redirect URI for callback handling.
/// Uses custom URI scheme for desktop application.
/// </summary>
public const string OAuthRedirectUri = "genhub://oauth/callback";

/// <summary>
/// OAuth 2.0 scope for user profile access.
/// </summary>
public const string OAuthScope = "user_profile";

/// <summary>
/// OAuth state validity period in minutes.
/// </summary>
public const int OAuthStateValidityMinutes = 10;

/// <summary>
/// Cache duration for tournament data in minutes.
/// </summary>
public const int CacheDurationMinutes = 5;

/// <summary>
/// Default request timeout in seconds.
/// </summary>
public const int DefaultRequestTimeoutSeconds = 30;

/// <summary>
/// Maximum retry attempts for failed requests.
/// </summary>
public const int MaxRetryAttempts = 3;

/// <summary>
/// Delay between retry attempts in milliseconds.
/// </summary>
public const int RetryDelayMs = 1000;

/// <summary>
/// Forum ID for Zero Hour General Discussion.
/// </summary>
public const string ZeroHourGeneralDiscussionForumId = "35";

/// <summary>
/// UI colors for tournament statuses.
/// </summary>
public static class Colors
Comment thread
undead2146 marked this conversation as resolved.
{
/// <summary>Signups are currently open.</summary>
public const string SignupsOpen = "#FF9800"; // Orange

/// <summary>Tournament is upcoming.</summary>
public const string Upcoming = "#F44336"; // Red

/// <summary>Tournament is currently active.</summary>
public const string Active = "#4CAF50"; // Green

/// <summary>Tournament has finished.</summary>
public const string Finished = "#9C27B0"; // Purple

/// <summary>Default status color.</summary>
public const string Default = "#757575"; // Gray
}
}
1 change: 1 addition & 0 deletions GenHub/GenHub.Core/GenHub.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="CsvHelper" />
<PackageReference Include="HtmlAgilityPack" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using GenHub.Core.Interfaces.GameReplays;
using GenHub.Core.Services.GameReplays;
using Microsoft.Extensions.DependencyInjection;

namespace GenHub.Infrastructure.DependencyInjection;

/// <summary>
/// Dependency injection module for GameReplays services.
/// Registers all GameReplays-related services with appropriate lifetimes.
/// </summary>
public static class GameReplaysModule
{
/// <summary>
/// Registers GameReplays services with service collection.
/// </summary>
/// <param name="services">The service collection to register services with.</param>
public static void RegisterGameReplaysServices(this IServiceCollection services)
{
// Register HTTP client as transient (creates new instance each time)
services.AddTransient<IGameReplaysHttpClient, GameReplaysHttpClient>();
Comment on lines +19 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

registering HttpClient as transient can cause socket exhaustion - use IHttpClientFactory instead with AddHttpClient<IGameReplaysHttpClient, GameReplaysHttpClient>()

Prompt To Fix With AI
This is a comment left during a code review.
Path: GenHub/GenHub.Core/Infrastructure/DependencyInjection/GameReplaysModule.cs
Line: 19:20

Comment:
registering `HttpClient` as transient can cause socket exhaustion - use `IHttpClientFactory` instead with `AddHttpClient<IGameReplaysHttpClient, GameReplaysHttpClient>()`

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +19 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transient HttpClient registration creates new instances on each request, leading to socket exhaustion under load

Suggested change
// Register HTTP client as transient (creates new instance each time)
services.AddTransient<IGameReplaysHttpClient, GameReplaysHttpClient>();
// Use IHttpClientFactory to manage HttpClient lifecycle properly
services.AddHttpClient<IGameReplaysHttpClient, GameReplaysHttpClient>();
Prompt To Fix With AI
This is a comment left during a code review.
Path: GenHub/GenHub.Core/Infrastructure/DependencyInjection/GameReplaysModule.cs
Line: 19:20

Comment:
transient `HttpClient` registration creates new instances on each request, leading to socket exhaustion under load

```suggestion
        // Use IHttpClientFactory to manage HttpClient lifecycle properly
        services.AddHttpClient<IGameReplaysHttpClient, GameReplaysHttpClient>();
```

How can I resolve this? If you propose a fix, please make it concise.


// Register parser as transient (stateless, can be created each time)
services.AddTransient<IGameReplaysParser, GameReplaysParser>();

// Register auth service as singleton (maintains OAuth state across app)
services.AddSingleton<IGameReplaysAuthService, GameReplaysAuthService>();

// Register comment service as transient
services.AddTransient<IGameReplaysCommentService, GameReplaysCommentService>();

// Register main service as scoped (one instance per request/scope)
services.AddScoped<IGameReplaysService, GameReplaysService>();
}
}
110 changes: 110 additions & 0 deletions GenHub/GenHub.Core/Interfaces/GameReplays/IGameReplaysAuthService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using GenHub.Core.Models.GameReplays;
using GenHub.Core.Models.Results;
using System.Threading.Tasks;

namespace GenHub.Core.Interfaces.GameReplays;

/// <summary>
/// OAuth 2.0 authentication service for GameReplays.
/// Handles login flow, token management, and user session.
/// </summary>
public interface IGameReplaysAuthService
{
/// <summary>
/// Gets the authorization URL for OAuth 2.0 login.
/// </summary>
/// <returns>Result containing the authorization URL.</returns>
OperationResult<string> GetAuthorizationUrl();

/// <summary>
/// Exchanges authorization code for access token.
/// </summary>
/// <param name="code">The authorization code from callback.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Result containing the OAuth token response.</returns>
Task<OperationResult<OAuthTokenResponse>> ExchangeCodeForTokenAsync(
string code,
CancellationToken cancellationToken = default);

/// <summary>
/// Refreshes the access token using refresh token.
/// </summary>
/// <param name="refreshToken">The refresh token.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Result containing the new OAuth token response.</returns>
Task<OperationResult<OAuthTokenResponse>> RefreshTokenAsync(
string refreshToken,
CancellationToken cancellationToken = default);

/// <summary>
/// Gets user info using the access token.
/// </summary>
/// <param name="accessToken">The access token.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Result containing the user info.</returns>
Task<OperationResult<OAuthUserInfo>> GetUserInfoAsync(
string accessToken,
CancellationToken cancellationToken = default);

/// <summary>
/// Validates the OAuth state parameter for CSRF protection.
/// </summary>
/// <param name="state">The state parameter from callback.</param>
/// <returns>True if state is valid, false otherwise.</returns>
bool ValidateState(string state);

/// <summary>
/// Generates a new OAuth state parameter.
/// </summary>
/// <returns>The generated state string.</returns>
string GenerateState();

/// <summary>
/// Saves the OAuth token response to secure storage.
/// </summary>
/// <param name="tokenResponse">The token response to save.</param>
/// <returns>Result indicating success or failure.</returns>
Task<OperationResult<bool>> SaveTokenAsync(OAuthTokenResponse tokenResponse);

/// <summary>
/// Loads the OAuth token response from secure storage.
/// </summary>
/// <returns>Result containing the token response or null if not found.</returns>
Task<OperationResult<OAuthTokenResponse?>> LoadTokenAsync();

/// <summary>
/// Clears the saved OAuth token from secure storage.
/// </summary>
/// <returns>Result indicating success or failure.</returns>
Task<OperationResult<bool>> ClearTokenAsync();

/// <summary>
/// Checks if the current token is expired or needs refresh.
/// </summary>
/// <returns>True if token is expired, false otherwise.</returns>
bool IsTokenExpired();

/// <summary>
/// Gets the current access token.
/// </summary>
/// <returns>The access token or null if not authenticated.</returns>
string? GetAccessToken();

/// <summary>
/// Gets the current refresh token.
/// </summary>
/// <returns>The refresh token or null if not available.</returns>
string? GetRefreshToken();

/// <summary>
/// Gets the current authenticated user info.
/// </summary>
/// <returns>The user info or null if not authenticated.</returns>
OAuthUserInfo? GetCurrentUser();

/// <summary>
/// Sets the current user info.
/// </summary>
/// <param name="userInfo">The user info to set.</param>
void SetCurrentUser(OAuthUserInfo userInfo);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using GenHub.Core.Models.GameReplays;
using GenHub.Core.Models.Results;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace GenHub.Core.Interfaces.GameReplays;

/// <summary>
/// Service for posting comments to GameReplays forum topics.
/// </summary>
public interface IGameReplaysCommentService
{
/// <summary>
/// Posts a comment to a forum topic.
/// </summary>
/// <param name="topicId">The tournament topic ID.</param>
/// <param name="comment">The comment content.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Result indicating success or failure.</returns>
Task<OperationResult<bool>> PostCommentAsync(
string topicId,
string comment,
CancellationToken cancellationToken = default);

/// <summary>
/// Gets comments for a forum topic.
/// TODO: Implement when GameReplays provides comments API.
/// </summary>
/// <param name="topicId">The tournament topic ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Result containing comment collection.</returns>
Task<OperationResult<IEnumerable<Comment>>> GetCommentsAsync(
string topicId,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using GenHub.Core.Models.GameReplays;
using GenHub.Core.Models.Results;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace GenHub.Core.Interfaces.GameReplays;

/// <summary>
/// HTTP client interface for GameReplays API and web scraping.
/// Handles cookie management and HTTP requests.
/// </summary>
public interface IGameReplaysHttpClient
{
/// <summary>
/// Gets HTML content from a URL.
/// </summary>
/// <param name="url">The URL to fetch.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Result containing HTML content.</returns>
Task<OperationResult<string>> GetHtmlAsync(
string url,
CancellationToken cancellationToken = default);

/// <summary>
/// Posts form data to a URL.
/// </summary>
/// <param name="url">The URL to post to.</param>
/// <param name="formData">The form data to post.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Result containing response content.</returns>
Task<OperationResult<string>> PostFormAsync(
string url,
Dictionary<string, string> formData,
CancellationToken cancellationToken = default);

/// <summary>
/// Gets JSON content from an API endpoint.
/// </summary>
/// <typeparam name="T">The type to deserialize to.</typeparam>
/// <param name="url">The API endpoint URL.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Result containing deserialized data.</returns>
Task<OperationResult<T>> GetJsonAsync<T>(
string url,
CancellationToken cancellationToken = default);

/// <summary>
/// Posts JSON data to an API endpoint.
/// </summary>
/// <typeparam name="TRequest">The request type.</typeparam>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <param name="url">The API endpoint URL.</param>
/// <param name="data">The data to post.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Result containing deserialized response.</returns>
Task<OperationResult<TResponse>> PostJsonAsync<TRequest, TResponse>(
string url,
TRequest data,
CancellationToken cancellationToken = default);

/// <summary>
/// Sets authentication cookie for requests.
/// </summary>
/// <param name="cookieValue">The cookie value.</param>
void SetAuthCookie(string cookieValue);

/// <summary>
/// Clears authentication cookie.
/// </summary>
void ClearAuthCookie();

/// <summary>
/// Gets the current authentication cookie value.
/// </summary>
/// <returns>The cookie value or null if not set.</returns>
string? GetAuthCookie();
}
Loading
Loading