diff --git a/README.md b/README.md index 24fee1c..fe6d4c6 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,12 @@ curl https://api.ipsimple.org/api/ip/v6 # Get all detected addresses curl https://api.ipsimple.org/api/ip/all +# Get geolocation for client IP +curl https://api.ipsimple.org/api/ip/geolocation +# Get geolocation for specific IP +curl https://api.ipsimple.org/api/ip/geolocation/203.0.113.1 +# Submit bulk IP processing job +curl -X POST https://api.ipsimple.org/api/ip/bulk -d '{"ips": ["203.0.113.1"]}' ``` ### Response Format @@ -140,8 +146,13 @@ Once running, you can test the following endpoints: - `GET /api/ip` - Get all IP address information in JSON format - `GET /api/ip/v4` - Get IPv4 address only -- `GET /api/ip/v6` - Get IPv6 address only +- `GET /api/ip/v6` - Get IPv6 address only - `GET /api/ip/all` - Get all detected IP addresses +- `GET /api/ip/geolocation` - Geolocation for client IP +- `GET /api/ip/geolocation/{ip}` - Geolocation for a specific IP +- `POST /api/ip/bulk` - Submit bulk IP processing job +- `GET /api/ip/bulk/{jobId}` - Check bulk job status +- `GET /api/ip/bulk/{jobId}/results` - Download bulk job results ### Testing the API diff --git a/src/IpSimple.PublicIp.Api/Program.cs b/src/IpSimple.PublicIp.Api/Program.cs index 557a08c..4dc73b4 100644 --- a/src/IpSimple.PublicIp.Api/Program.cs +++ b/src/IpSimple.PublicIp.Api/Program.cs @@ -18,6 +18,8 @@ public static WebApplication CreateWebApp(string[] args) builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); var app = builder.Build(); @@ -28,6 +30,8 @@ public static WebApplication CreateWebApp(string[] args) } var ipAddressService = app.Services.GetRequiredService(); + var geolocationService = app.Services.GetRequiredService(); + var bulkService = app.Services.GetRequiredService(); app.MapGet("/", ipAddressService.GetClientIpv4) .WithName("Default") @@ -56,6 +60,15 @@ public static WebApplication CreateWebApp(string[] args) return operation; }); + app.MapGet("/all", ipAddressService.GetClientIps) + .WithName("GetDualStackIps") + .WithOpenApi(operation => + { + operation.Summary = "Dual stack IP addresses"; + operation.Description = "Returns both IPv4 and IPv6 addresses if available."; + return operation; + }); + app.MapGet("/ipv6", ipAddressService.GetClientIpv6) .WithName("GetPublicIPv6") .WithOpenApi(operation => @@ -74,6 +87,51 @@ public static WebApplication CreateWebApp(string[] args) return operation; }); + app.MapGet("/geolocation", geolocationService.GetGeolocationForClient) + .WithName("GetClientGeolocation") + .WithOpenApi(operation => + { + operation.Summary = "Geolocation for client IP"; + operation.Description = "Returns geolocation metadata for the calling client."; + return operation; + }); + + app.MapGet("/geolocation/{ipAddress}", geolocationService.GetGeolocationForIp) + .WithName("GetIpGeolocation") + .WithOpenApi(operation => + { + operation.Summary = "Geolocation lookup"; + operation.Description = "Provides geolocation metadata for a specific IP address."; + return operation; + }); + + app.MapPost("/bulk", bulkService.SubmitBulkJob) + .WithName("SubmitBulkIpJob") + .WithOpenApi(operation => + { + operation.Summary = "Submit bulk IP processing"; + operation.Description = "Accepts a list of IP addresses for asynchronous processing."; + return operation; + }); + + app.MapGet("/bulk/{jobId}", bulkService.GetJobStatus) + .WithName("GetBulkJobStatus") + .WithOpenApi(operation => + { + operation.Summary = "Bulk job status"; + operation.Description = "Retrieves processing status for a submitted bulk IP job."; + return operation; + }); + + app.MapGet("/bulk/{jobId}/results", bulkService.GetJobResults) + .WithName("GetBulkJobResults") + .WithOpenApi(operation => + { + operation.Summary = "Bulk job results"; + operation.Description = "Gets processed results for a completed bulk IP job."; + return operation; + }); + app.MapGet("/version", () => { var version = Environment.GetEnvironmentVariable("APP_VERSION") ?? "unknown"; diff --git a/src/IpSimple.PublicIp.Api/Services/BulkIpProcessingService.cs b/src/IpSimple.PublicIp.Api/Services/BulkIpProcessingService.cs new file mode 100644 index 0000000..b2aa321 --- /dev/null +++ b/src/IpSimple.PublicIp.Api/Services/BulkIpProcessingService.cs @@ -0,0 +1,28 @@ +using IpSimple.Domain.Settings; + +namespace IpSimple.PublicIp.Api.Services; + +public class BulkIpProcessingService : IBulkIpProcessingService +{ + public IResult SubmitBulkJob(HttpContext httpContext) + { + // TODO: Parse incoming IP list from body or uploaded file. + // Queue background job for asynchronous processing (see issue #15). + // Return job identifier for client to poll. + return Results.Json(new { message = "Bulk IP processing not yet implemented" }, JsonSerializerSettings.DefaultJsonSerializer); + } + + public IResult GetJobStatus(string jobId) + { + // TODO: Retrieve job status from persistent store or in-memory cache. + // Provide percentage complete and any available metadata. + return Results.Json(new { jobId, status = "pending" }, JsonSerializerSettings.DefaultJsonSerializer); + } + + public IResult GetJobResults(string jobId) + { + // TODO: Return aggregated results for completed job. + // Support download of large result sets and handle pagination. + return Results.Json(new { jobId, results = Array.Empty() }, JsonSerializerSettings.DefaultJsonSerializer); + } +} diff --git a/src/IpSimple.PublicIp.Api/Services/GeolocationService.cs b/src/IpSimple.PublicIp.Api/Services/GeolocationService.cs new file mode 100644 index 0000000..0485637 --- /dev/null +++ b/src/IpSimple.PublicIp.Api/Services/GeolocationService.cs @@ -0,0 +1,23 @@ +using IpSimple.Domain.Settings; +using IpSimple.Extensions; + +namespace IpSimple.PublicIp.Api.Services; + +public class GeolocationService : IGeolocationService +{ + public IResult GetGeolocationForClient(HttpContext httpContext) + { + // TODO: Extract client IP using existing helpers + // and call external geolocation provider (e.g. MaxMind). + // Return structured geolocation data similar to issue #14 spec. + return Results.Json(new { message = "Geolocation lookup not yet implemented" }, JsonSerializerSettings.DefaultJsonSerializer); + } + + public IResult GetGeolocationForIp(string ipAddress) + { + // TODO: Validate the provided IP address and query + // the geolocation provider for metadata. Cache results + // and handle provider errors appropriately. + return Results.Json(new { message = "Geolocation lookup not yet implemented", ipAddress }, JsonSerializerSettings.DefaultJsonSerializer); + } +} diff --git a/src/IpSimple.PublicIp.Api/Services/IBulkIpProcessingService.cs b/src/IpSimple.PublicIp.Api/Services/IBulkIpProcessingService.cs new file mode 100644 index 0000000..248ced9 --- /dev/null +++ b/src/IpSimple.PublicIp.Api/Services/IBulkIpProcessingService.cs @@ -0,0 +1,8 @@ +namespace IpSimple.PublicIp.Api.Services; + +public interface IBulkIpProcessingService +{ + IResult SubmitBulkJob(HttpContext httpContext); + IResult GetJobStatus(string jobId); + IResult GetJobResults(string jobId); +} diff --git a/src/IpSimple.PublicIp.Api/Services/IGeolocationService.cs b/src/IpSimple.PublicIp.Api/Services/IGeolocationService.cs new file mode 100644 index 0000000..adf19d1 --- /dev/null +++ b/src/IpSimple.PublicIp.Api/Services/IGeolocationService.cs @@ -0,0 +1,7 @@ +namespace IpSimple.PublicIp.Api.Services; + +public interface IGeolocationService +{ + IResult GetGeolocationForClient(HttpContext httpContext); + IResult GetGeolocationForIp(string ipAddress); +} diff --git a/src/IpSimple.PublicIp.Api/Services/IIpAddressService.cs b/src/IpSimple.PublicIp.Api/Services/IIpAddressService.cs index dd10a66..562c012 100644 --- a/src/IpSimple.PublicIp.Api/Services/IIpAddressService.cs +++ b/src/IpSimple.PublicIp.Api/Services/IIpAddressService.cs @@ -4,6 +4,7 @@ public interface IIpAddressService { IResult GetClientIp(HttpContext httpContext, bool getAllXForwardedForIpAddresses = false); IResult GetAllClientIps(HttpContext httpContext); + IResult GetClientIps(HttpContext httpContext); IResult GetClientIpv4(HttpContext httpContext, bool getAllXForwardedForIpAddresses = false); IResult GetAllClientIpv4s(HttpContext httpContext); IResult GetClientIpv6(HttpContext httpContext, bool getAllXForwardedForIpAddresses = false); diff --git a/src/IpSimple.PublicIp.Api/Services/IpAddressService.cs b/src/IpSimple.PublicIp.Api/Services/IpAddressService.cs index e0aaedf..28f05d5 100644 --- a/src/IpSimple.PublicIp.Api/Services/IpAddressService.cs +++ b/src/IpSimple.PublicIp.Api/Services/IpAddressService.cs @@ -1,6 +1,7 @@ using IpSimple.Domain; using IpSimple.Domain.Settings; using IpSimple.Extensions; +using System.Linq; namespace IpSimple.PublicIp.Api.Services; @@ -23,6 +24,25 @@ public IResult GetClientIp(HttpContext httpContext, bool getAllXForwardedForIpAd public IResult GetAllClientIps(HttpContext httpContext) => GetClientIp(httpContext, true); + public IResult GetClientIps(HttpContext httpContext) + { + // Combine IPv4 and IPv6 detection using existing helpers + // so callers can retrieve both addresses in a single call + var ipv4 = httpContext.GetClientIpv4Address(); + var ipv6 = httpContext.GetClientIpv6Address(); + + var format = httpContext.Request.Query["format"].ToString(); + + if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase)) + { + // TODO: extend model to match issue #5 specification + return Results.Json(new { ipv4, ipv6 }, JsonSerializerSettings.DefaultJsonSerializer, "application/json"); + } + + var combined = string.Join(", ", new[] { ipv4, ipv6 }.Where(s => !string.IsNullOrWhiteSpace(s))); + return Results.Text(combined, "text/plain"); + } + public IResult GetClientIpv4(HttpContext httpContext, bool getAllXForwardedForIpAddresses = false) { var clientIp = getAllXForwardedForIpAddresses ? httpContext.GetAllPossibleClientIpv4Addresses() : httpContext.GetClientIpv4Address(); diff --git a/src/IpSimple.PublicIp.Api/api-spec.yml b/src/IpSimple.PublicIp.Api/api-spec.yml index e6906da..c208757 100644 --- a/src/IpSimple.PublicIp.Api/api-spec.yml +++ b/src/IpSimple.PublicIp.Api/api-spec.yml @@ -123,3 +123,112 @@ paths: example: ["2a00:1450:400f:80d::200e", "2a00:1450:400f:80d::200f"] '429': description: Too many requests + /all: + get: + summary: Dual stack IP addresses + description: Returns both IPv4 and IPv6 addresses if available. + parameters: + - in: query + name: format + schema: + type: string + enum: [json, plain] + required: false + responses: + '200': + description: Successful response + content: + text/plain: + schema: + type: string + example: "203.0.113.1, 2001:db8::1" + application/json: + schema: + type: object + properties: + ipv4: + type: string + example: "203.0.113.1" + ipv6: + type: string + example: "2001:db8::1" + '429': + description: Too many requests + /geolocation: + get: + summary: Geolocation for client IP + description: Returns geolocation metadata for the calling client. + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + example: {"message": "Geolocation lookup not yet implemented"} + /geolocation/{ipAddress}: + get: + summary: Geolocation lookup + description: Provides geolocation metadata for a specific IP address. + parameters: + - in: path + name: ipAddress + required: true + schema: + type: string + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + example: {"message": "Geolocation lookup not yet implemented"} + /bulk: + post: + summary: Submit bulk IP processing + description: Accepts a list of IP addresses for asynchronous processing. + responses: + '202': + description: Accepted + content: + application/json: + schema: + type: object + example: {"message": "Bulk IP processing not yet implemented"} + /bulk/{jobId}: + get: + summary: Bulk job status + description: Retrieves processing status for a submitted bulk IP job. + parameters: + - in: path + name: jobId + required: true + schema: + type: string + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + example: {"jobId": "bulk-123", "status": "pending"} + /bulk/{jobId}/results: + get: + summary: Bulk job results + description: Gets processed results for a completed bulk IP job. + parameters: + - in: path + name: jobId + required: true + schema: + type: string + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + example: {"jobId": "bulk-123", "results": []}