Compare commits

...

2 Commits

11 changed files with 182 additions and 15 deletions

View File

@ -256,8 +256,8 @@ router.get('/version', (_req: Request, res: Response): void => {
try {
res.json({
version: process.env.APP_VERSION || '1.0.0',
createdAt: process.env.CREATED_AT || 'unknown',
gitRevision: process.env.GIT_REVISION || 'unknown',
buildDate: process.env.CREATED_AT || 'unknown',
commitHash: process.env.GIT_REVISION || 'unknown',
service: 'Bitip GeoIP Service',
});
} catch (error) {

View File

@ -1,7 +0,0 @@
namespace Bitip.Client
{
public class Class1
{
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) 2025 Tudor Stanciu
namespace Bitip.Client.Constants
{
internal struct ApiKeys
{
public const string HttpHeader = "X-API-Key";
}
internal struct ApiRoutes
{
public const string
Health = "health",
Version = "version";
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) 2025 Tudor Stanciu
using Bitip.Client.Models;
using Bitip.Client.Services;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace Bitip.Client
{
public static class DependencyInjectionExtension
{
public static void UseBitipClient(this IServiceCollection services, string baseUrl, string apiKey)
{
if (string.IsNullOrWhiteSpace(baseUrl))
throw new ArgumentException("Value cannot be null or whitespace.", nameof(baseUrl));
if (string.IsNullOrWhiteSpace(apiKey))
throw new ArgumentException("Value cannot be null or whitespace.", nameof(apiKey));
services.Configure<BitipOptions>(options =>
{
options.BaseUrl = baseUrl;
options.ApiKey = apiKey;
});
services.AddHttpClient<IBitipClient, BitipClient>();
}
}
}

View File

@ -0,0 +1,24 @@
using Bitip.Client.Models;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Bitip.Client.Extensions
{
internal static class HttpMessageExtensions
{
public static async Task<HttpResponseMessage> EnsureSuccessOperation(this HttpResponseMessage response, CancellationToken cancellationToken = default)
{
if (response.IsSuccessStatusCode)
return response;
var errorResponse = await response.Content.ReadFromJsonAsync<ErrorResponse>(cancellationToken);
if (errorResponse is null)
throw new HttpRequestException("An error occurred while fetching Bitip information.");
var message = $"{errorResponse.Error}: {errorResponse.Message}";
if (!string.IsNullOrWhiteSpace(errorResponse.Ip))
message += $" (IP: {errorResponse.Ip})";
throw new HttpRequestException(message);
}
}
}

View File

@ -0,0 +1,10 @@
// Copyright (c) 2025 Tudor Stanciu
namespace Bitip.Client.Models
{
internal record BitipOptions
{
public required string BaseUrl { get; set; }
public required string ApiKey { get; set; }
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) 2025 Tudor Stanciu
using System;
namespace Bitip.Client.Models
{
internal record HealthResponse
{
public required string Status { get; init; }
public required string Service { get; init; }
public DateTime Timestamp { get; init; }
public string? Error { get; init; }
}
internal record VersionResponse
{
public required string Version { get; init; }
public required string CommitHash { get; init; }
public DateTime BuildDate { get; init; }
}
internal record ErrorResponse
{
public required string Error { get; init; }
public required string Message { get; init; }
public string? Ip { get; init; }
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) 2025 Tudor Stanciu
using Bitip.Client.Constants;
using Bitip.Client.Extensions;
using Bitip.Client.Models;
using Microsoft.Extensions.Options;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Bitip.Client.Services
{
internal class BitipClient : IBitipClient
{
private readonly HttpClient _httpClient;
public BitipClient(HttpClient httpClient, IOptions<BitipOptions> options)
{
httpClient.BaseAddress = EnsureTrailingSlash(options.Value.BaseUrl);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpClient.DefaultRequestHeaders.Add(ApiKeys.HttpHeader, options.Value.ApiKey);
_httpClient = httpClient;
}
public async Task<HealthResponse> GetHealth(CancellationToken cancellationToken = default)
{
var response = await _httpClient.GetAsync(ApiRoutes.Health, cancellationToken);
await response.EnsureSuccessOperation(cancellationToken);
var data = await response.Content.ReadFromJsonAsync<HealthResponse>(cancellationToken);
if (data is null)
throw new InvalidOperationException("The response content is null.");
return data;
}
public async Task<VersionResponse> GetVersion(CancellationToken cancellationToken = default)
{
var response = await _httpClient.GetAsync(ApiRoutes.Version, cancellationToken);
await response.EnsureSuccessOperation(cancellationToken);
var data = await response.Content.ReadFromJsonAsync<VersionResponse>(cancellationToken);
if (data is null)
throw new InvalidOperationException("The response content is null.");
return data;
}
private Uri EnsureTrailingSlash(string url)
{
var address = url.EndsWith("/") ? url : $"{url}/";
return new Uri(address);
}
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) 2025 Tudor Stanciu
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bitip.Client.Services
{
public interface IBitipClient
{
}
}

View File

@ -11,8 +11,8 @@ import { pathCombine } from '@flare/utiliyo';
interface VersionInfo {
version: string;
createdAt: string;
gitRevision: string;
buildDate: string;
commitHash: string;
service: string;
}
@ -90,8 +90,8 @@ const App: React.FC = () => {
{versionInfo && (
<p className="version-info">
Version {versionInfo.version}
{versionInfo.createdAt !== 'unknown' &&
` | Released ${new Date(versionInfo.createdAt).toLocaleString()}`}
{versionInfo.buildDate !== 'unknown' &&
` | Released ${new Date(versionInfo.buildDate).toLocaleString()}`}
</p>
)}
</footer>

View File

@ -67,8 +67,8 @@ export interface IPResponse {
export interface VersionInfo {
version: string;
createdAt: string;
gitRevision: string;
buildDate: string;
commitHash: string;
service: string;
}