diff --git a/src/AzureOpenAIProxy.ApiApp/Controllers/ChatCompletionsController.cs b/src/AzureOpenAIProxy.ApiApp/Controllers/ChatCompletionsController.cs
index c857554a..b218cea9 100644
--- a/src/AzureOpenAIProxy.ApiApp/Controllers/ChatCompletionsController.cs
+++ b/src/AzureOpenAIProxy.ApiApp/Controllers/ChatCompletionsController.cs
@@ -16,7 +16,7 @@ namespace AzureOpenAIProxy.ApiApp.Controllers;
/// instance.
/// instance.
[ApiController]
-[Route("openai")]
+[Route("api/openai")]
public class ChatCompletionsController(
[FromKeyedServices("accesscode")] IAuthService auth,
IOpenAIService openai,
diff --git a/src/AzureOpenAIProxy.ApiApp/Controllers/CompletionsController.cs b/src/AzureOpenAIProxy.ApiApp/Controllers/CompletionsController.cs
index 31536143..9086529d 100644
--- a/src/AzureOpenAIProxy.ApiApp/Controllers/CompletionsController.cs
+++ b/src/AzureOpenAIProxy.ApiApp/Controllers/CompletionsController.cs
@@ -16,7 +16,7 @@ namespace AzureOpenAIProxy.ApiApp.Controllers;
/// instance.
/// instance.
[ApiController]
-[Route("openai")]
+[Route("api/openai")]
public class CompletionsController(
[FromKeyedServices("accesscode")] IAuthService auth,
IOpenAIService openai,
diff --git a/src/AzureOpenAIProxy.ApiApp/Controllers/ManagementController.cs b/src/AzureOpenAIProxy.ApiApp/Controllers/ManagementController.cs
index 4d23bbde..5093a5ac 100644
--- a/src/AzureOpenAIProxy.ApiApp/Controllers/ManagementController.cs
+++ b/src/AzureOpenAIProxy.ApiApp/Controllers/ManagementController.cs
@@ -12,32 +12,85 @@ namespace AzureOpenAIProxy.ApiApp.Controllers;
/// instance.
/// instance.
[ApiController]
-[Route("management")]
+[Route("api/management")]
public class ManagementController(
- [FromKeyedServices("management")] IAuthService auth,
+ [FromKeyedServices("management")] IAuthService auth,
IManagementService management,
ILogger logger) : ControllerBase
{
- private readonly IAuthService _auth = auth ?? throw new ArgumentNullException(nameof(auth));
+ private readonly IAuthService _auth = auth ?? throw new ArgumentNullException(nameof(auth));
private readonly IManagementService _management = management ?? throw new ArgumentNullException(nameof(management));
private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
[HttpGet("events", Name = "GetListOfEvents")]
- public async Task GetEventsAsync([FromHeader(Name = "Authorization")] string apiKey)
+ public async Task GetEventsAsync(
+ [FromHeader(Name = "Authorization")] string apiKey,
+ [FromQuery(Name = "page")] int? page = 0,
+ [FromQuery(Name = "size")] int? size = 20
+ )
{
- var result = new OkObjectResult("Pong");
+ this._logger.LogInformation("Received a request to get all events");
- return await Task.FromResult(result);
+ var record = await this._auth.ValidateAsync(apiKey);
+ if (record == null)
+ {
+ this._logger.LogError("Invalid API key");
+
+ return new UnauthorizedResult();
+ }
+
+ try
+ {
+ var result = await this._management.GetEventsAsync(page, size);
+
+ this._logger.LogInformation("Completed the request to get all event");
+
+ return new OkObjectResult(result);
+ }
+ catch (Exception ex)
+ {
+ this._logger.LogError(ex, "Failed to get all event");
+
+ return new ObjectResult(ex.Message) { StatusCode = StatusCodes.Status500InternalServerError };
+ }
}
[HttpPost("events", Name = "CreateEvent")]
public async Task CreateEvent(
[FromHeader(Name = "Authorization")] string apiKey,
- [FromBody] ManagementRequest req)
+ [FromBody] EventRequest req)
{
- var result = new OkObjectResult("Pong");
+ this._logger.LogInformation("Received a request to generate an event");
- return await Task.FromResult(result);
+ var record = await this._auth.ValidateAsync(apiKey);
+ if (record == null)
+ {
+ this._logger.LogError("Invalid API key");
+
+ return new UnauthorizedResult();
+ }
+
+ if (req == null)
+ {
+ this._logger.LogError("No request payload");
+
+ return new BadRequestResult();
+ }
+
+ try
+ {
+ var result = await this._management.CreateEventAsync(req);
+
+ this._logger.LogInformation("Completed the request to generate the event");
+
+ return new OkObjectResult(result);
+ }
+ catch (Exception ex)
+ {
+ this._logger.LogError(ex, "Failed to generate the event");
+
+ return new ObjectResult(ex.Message) { StatusCode = StatusCodes.Status500InternalServerError };
+ }
}
[HttpGet("events/{eventId}", Name = "GetEventById")]
@@ -45,9 +98,37 @@ public async Task GetEventByEventIdAsync(
[FromHeader(Name = "Authorization")] string apiKey,
[FromRoute] string eventId)
{
- var result = new OkObjectResult("Pong");
+ this._logger.LogInformation("Received a request to get an event details by event ID");
- return await Task.FromResult(result);
+ var record = await this._auth.ValidateAsync(apiKey);
+ if (record == null)
+ {
+ this._logger.LogError("Invalid API key");
+
+ return new UnauthorizedResult();
+ }
+
+ if (string.IsNullOrWhiteSpace(eventId))
+ {
+ this._logger.LogError("No event ID");
+
+ return new NotFoundResult();
+ }
+
+ try
+ {
+ var result = await this._management.GetEvenByIdAsync(eventId);
+
+ this._logger.LogInformation("Completed the request to get an event details by event ID");
+
+ return new OkObjectResult(result);
+ }
+ catch (Exception ex)
+ {
+ this._logger.LogError(ex, "Failed to get an event details by event ID");
+
+ return new ObjectResult(ex.Message) { StatusCode = StatusCodes.Status500InternalServerError };
+ }
}
[HttpGet("events/{eventId}/access-codes", Name = "GetListOfEventAccessCodes")]
@@ -62,8 +143,8 @@ public async Task GetAccessCodesByEventIdAsync(
[HttpPost("events/{eventId}/access-codes", Name = "CreateEventAccessCode")]
public async Task CreateEvent(
- [FromRoute] string eventId,
[FromHeader(Name = "Authorization")] string apiKey,
+ [FromRoute] string eventId,
[FromBody] AccessCodeRequest req)
{
this._logger.LogInformation("Received a request to generate an access code");
@@ -76,6 +157,20 @@ public async Task CreateEvent(
return new UnauthorizedResult();
}
+ if (string.IsNullOrWhiteSpace(eventId))
+ {
+ this._logger.LogError("No event ID");
+
+ return new NotFoundResult();
+ }
+
+ if (req == null)
+ {
+ this._logger.LogError("No request payload");
+
+ return new BadRequestResult();
+ }
+
try
{
req.EventId = eventId;
@@ -92,4 +187,15 @@ public async Task CreateEvent(
return new ObjectResult(ex.Message) { StatusCode = StatusCodes.Status500InternalServerError };
}
}
+
+ [HttpGet("events/{eventId}/access-codes/{gitHubAlias}", Name = "GetEventAccessCodeByGitHubAlias")]
+ public async Task GetAccessCodesByEventIdAsync(
+ [FromHeader(Name = "Authorization")] string apiKey,
+ [FromRoute] string eventId,
+ [FromRoute] string gitHubAlias)
+ {
+ var result = new OkObjectResult("Pong");
+
+ return await Task.FromResult(result);
+ }
}
diff --git a/src/AzureOpenAIProxy.ApiApp/Models/AccessCodeRecord.cs b/src/AzureOpenAIProxy.ApiApp/Models/AccessCodeRecord.cs
index c9193f41..e3b0de8c 100644
--- a/src/AzureOpenAIProxy.ApiApp/Models/AccessCodeRecord.cs
+++ b/src/AzureOpenAIProxy.ApiApp/Models/AccessCodeRecord.cs
@@ -45,17 +45,17 @@ public class AccessCodeRecord : ITableEntity
public string? ApiKey { get; set; }
///
- /// Gets or sets the date created.
+ /// Gets or sets the date created in UTC.
///
public DateTimeOffset? DateCreated { get; set; }
///
- /// Gets or sets the event date start.
+ /// Gets or sets the event date start in UTC.
///
public DateTimeOffset? EventDateStart { get; set; }
///
- /// Gets or sets the event date end.
+ /// Gets or sets the event date end in UTC.
///
public DateTimeOffset? EventDateEnd { get; set; }
diff --git a/src/AzureOpenAIProxy.ApiApp/Models/ManagementRecord.cs b/src/AzureOpenAIProxy.ApiApp/Models/EventRecord.cs
similarity index 87%
rename from src/AzureOpenAIProxy.ApiApp/Models/ManagementRecord.cs
rename to src/AzureOpenAIProxy.ApiApp/Models/EventRecord.cs
index 3bf63bb7..92cacd5e 100644
--- a/src/AzureOpenAIProxy.ApiApp/Models/ManagementRecord.cs
+++ b/src/AzureOpenAIProxy.ApiApp/Models/EventRecord.cs
@@ -4,9 +4,9 @@
namespace AzureOpenAIProxy.ApiApp.Models;
///
-/// This represents the table entity for management.
+/// This represents the table entity for event.
///
-public class ManagementRecord : ITableEntity
+public class EventRecord : ITableEntity
{
///
public string? PartitionKey { get; set; }
@@ -15,7 +15,7 @@ public class ManagementRecord : ITableEntity
public string? RowKey { get; set; }
///
- /// Gets or sets the event ID. It's equivalent to .
+ /// Gets or sets the event ID. It's equivalent to .
///
public string? EventId { get; set; }
@@ -40,12 +40,12 @@ public class ManagementRecord : ITableEntity
public string? EventOrganiserEmail { get; set; }
///
- /// Gets or sets the event start date.
+ /// Gets or sets the event start date in UTC.
///
public DateTimeOffset? EventDateStart { get; set; } = DateTimeOffset.MinValue;
///
- /// Gets or sets the event end date.
+ /// Gets or sets the event end date in UTC.
///
public DateTimeOffset? EventDateEnd { get; set; } = DateTimeOffset.MaxValue;
diff --git a/src/AzureOpenAIProxy.ApiApp/Models/ManagementRequest.cs b/src/AzureOpenAIProxy.ApiApp/Models/EventRequest.cs
similarity index 93%
rename from src/AzureOpenAIProxy.ApiApp/Models/ManagementRequest.cs
rename to src/AzureOpenAIProxy.ApiApp/Models/EventRequest.cs
index 3e7aef0c..76cf7cb3 100644
--- a/src/AzureOpenAIProxy.ApiApp/Models/ManagementRequest.cs
+++ b/src/AzureOpenAIProxy.ApiApp/Models/EventRequest.cs
@@ -3,9 +3,9 @@
namespace AzureOpenAIProxy.ApiApp.Models;
///
-/// This represents the request entity for management.
+/// This represents the request entity for event.
///
-public class ManagementRequest
+public class EventRequest
{
///
/// Gets or sets the event name.
diff --git a/src/AzureOpenAIProxy.ApiApp/Models/ManagementResponse.cs b/src/AzureOpenAIProxy.ApiApp/Models/EventResponse.cs
similarity index 67%
rename from src/AzureOpenAIProxy.ApiApp/Models/ManagementResponse.cs
rename to src/AzureOpenAIProxy.ApiApp/Models/EventResponse.cs
index cd657e2e..ce8bad82 100644
--- a/src/AzureOpenAIProxy.ApiApp/Models/ManagementResponse.cs
+++ b/src/AzureOpenAIProxy.ApiApp/Models/EventResponse.cs
@@ -3,22 +3,22 @@
namespace AzureOpenAIProxy.ApiApp.Models;
///
-/// This represents the response entity for management.
+/// This represents the response entity for event.
///
-public class ManagementResponse : ManagementRequest
+public class EventResponse : EventRequest
{
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
- public ManagementResponse()
+ public EventResponse()
{
}
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
- /// instance.
- public ManagementResponse(ManagementRecord record)
+ /// instance.
+ public EventResponse(EventRecord record)
{
this.Id = record.EventId;
this.AccessCode = record.ApiKey;
diff --git a/src/AzureOpenAIProxy.ApiApp/Models/EventResponseCollection.cs b/src/AzureOpenAIProxy.ApiApp/Models/EventResponseCollection.cs
new file mode 100644
index 00000000..56384486
--- /dev/null
+++ b/src/AzureOpenAIProxy.ApiApp/Models/EventResponseCollection.cs
@@ -0,0 +1,27 @@
+namespace AzureOpenAIProxy.ApiApp.Models;
+
+///
+/// This represents the response entity collection for event.
+///
+public class EventResponseCollection
+{
+ ///
+ /// Gets or sets the current page number.
+ ///
+ public int? CurrentPage { get; set; }
+
+ ///
+ /// Gets or sets the page size.
+ ///
+ public int? PageSize { get; set; }
+
+ ///
+ /// Gets or sets the total number of items.
+ ///
+ public int? Total { get; set; }
+
+ ///
+ /// Gets or sets the list of instances.
+ ///
+ public List Items { get; set; } = [];
+}
diff --git a/src/AzureOpenAIProxy.ApiApp/Program.cs b/src/AzureOpenAIProxy.ApiApp/Program.cs
index 16e98421..9065399a 100644
--- a/src/AzureOpenAIProxy.ApiApp/Program.cs
+++ b/src/AzureOpenAIProxy.ApiApp/Program.cs
@@ -10,8 +10,9 @@
// Add services to the container.
builder.Services.AddSingleton(p => p.GetService().GetSection(AoaiSettings.Name).Get());
builder.Services.AddHttpClient();
+builder.Services.AddScoped();
builder.Services.AddKeyedScoped, UserAuthService>("accesscode");
-builder.Services.AddKeyedScoped, ManagementAuthService>("management");
+builder.Services.AddKeyedScoped, ManagementAuthService>("management");
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
diff --git a/src/AzureOpenAIProxy.ApiApp/Services/ManagementAuthService.cs b/src/AzureOpenAIProxy.ApiApp/Services/ManagementAuthService.cs
index 7d695ef0..115d109e 100644
--- a/src/AzureOpenAIProxy.ApiApp/Services/ManagementAuthService.cs
+++ b/src/AzureOpenAIProxy.ApiApp/Services/ManagementAuthService.cs
@@ -9,23 +9,24 @@ namespace AzureOpenAIProxy.ApiApp.Services;
///
/// instance.
/// instance.
-public class ManagementAuthService(TableServiceClient client, ILogger logger) : AuthService
+public class ManagementAuthService(TableServiceClient client, ILogger logger) : AuthService
{
private const string TableName = "managements";
- private const string PartitionKeys = "master,management";
+ private const string PartitionKeyMaster = "master";
+ private const string PartitionKeyManagement = "management";
private readonly TableClient _table = client?.GetTableClient(TableName) ?? throw new ArgumentNullException(nameof(client));
private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
///
- public override async Task ValidateAsync(string apiKey)
+ public override async Task ValidateAsync(string apiKey)
{
- var partitionKeys = PartitionKeys.Split(',', StringSplitOptions.RemoveEmptyEntries);
var results = this._table
- .QueryAsync(p => partitionKeys.Contains(p.PartitionKey)
- && p.ApiKey == apiKey);
+ .QueryAsync(p => (p.PartitionKey.Equals(PartitionKeyMaster, StringComparison.InvariantCultureIgnoreCase) ||
+ p.PartitionKey.Equals(PartitionKeyManagement, StringComparison.InvariantCultureIgnoreCase))
+ && p.ApiKey == apiKey);
- var record = default(ManagementRecord);
+ var record = default(EventRecord);
await foreach (var result in results.AsPages())
{
if (result.Values.Count != 1)
@@ -39,7 +40,7 @@ record = result.Values.Single();
var now = DateTimeOffset.UtcNow;
- if (record.PartitionKey == "master")
+ if (record.PartitionKey.Equals(PartitionKeyMaster, StringComparison.InvariantCultureIgnoreCase))
{
return record;
}
@@ -49,6 +50,6 @@ record = result.Values.Single();
return record;
}
- return default(ManagementRecord);
+ return default(EventRecord);
}
}
diff --git a/src/AzureOpenAIProxy.ApiApp/Services/ManagementService.cs b/src/AzureOpenAIProxy.ApiApp/Services/ManagementService.cs
index 86286483..1e5923a1 100644
--- a/src/AzureOpenAIProxy.ApiApp/Services/ManagementService.cs
+++ b/src/AzureOpenAIProxy.ApiApp/Services/ManagementService.cs
@@ -1,6 +1,4 @@
-using System.Text.Json;
-
-using Azure.Data.Tables;
+using Azure.Data.Tables;
using AzureOpenAIProxy.ApiApp.Models;
@@ -11,6 +9,28 @@ namespace AzureOpenAIProxy.ApiApp.Services;
///
public interface IManagementService
{
+ ///
+ /// Gets the list of events.
+ ///
+ /// Page number.
+ /// Page size.
+ /// Returns the instance.
+ Task GetEventsAsync(int? page = 0, int? size = 20);
+
+ ///
+ /// Creates the event.
+ ///
+ /// instance.
+ /// Returns the instance.
+ Task CreateEventAsync(EventRequest req);
+
+ ///
+ /// Get the event by ID.
+ ///
+ /// Event ID.
+ /// Returns the instance.
+ Task GetEvenByIdAsync(string eventId);
+
///
/// Creates the access code.
///
@@ -34,18 +54,80 @@ public class ManagementService(TableServiceClient client, ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ ///
+ public async Task GetEventsAsync(int? page = 0, int? size = 20)
+ {
+ var results = this._managements.QueryAsync(p => p.PartitionKey
+ .Equals(ManagementsTablePartitionKey, StringComparison.InvariantCultureIgnoreCase));
+ var records = new List();
+ await foreach (var result in results.AsPages())
+ {
+ records.AddRange(result.Values.Select(p => new EventResponse(p)));
+ }
+
+ var skip = page.Value * size.Value;
+ var take = size.Value;
+
+ var response = new EventResponseCollection()
+ {
+ CurrentPage = page,
+ PageSize = size,
+ Total = records.Count,
+ Items = records.Skip(skip).Take(take).ToList(),
+ };
+
+ return response;
+ }
+
+ ///
+ public async Task CreateEventAsync(EventRequest req)
+ {
+ var eventId = Guid.NewGuid().ToString();
+ var apiKey = Guid.NewGuid().ToString();
+ var record = new EventRecord()
+ {
+ PartitionKey = ManagementsTablePartitionKey,
+ RowKey = eventId,
+ EventId = eventId,
+ EventName = req.EventName,
+ EventDescription = req.EventDescription,
+ EventOrganiser = req.EventOrganiser,
+ EventOrganiserEmail = req.EventOrganiserEmail,
+ EventDateStart = req.EventDateStart.Value.ToUniversalTime(),
+ EventDateEnd = req.EventDateEnd.Value.ToUniversalTime(),
+ ApiKey = apiKey,
+ };
+
+ await this._managements.UpsertEntityAsync(record).ConfigureAwait(false);
+ var result = await this._managements.GetEntityIfExistsAsync(ManagementsTablePartitionKey, eventId).ConfigureAwait(false);
+
+ return new EventResponse(result.Value);
+ }
+
+ ///
+ public async Task GetEvenByIdAsync(string eventId)
+ {
+ var result = await this._managements.GetEntityIfExistsAsync(
+ ManagementsTablePartitionKey,
+ eventId ?? throw new ArgumentNullException(nameof(eventId)))
+ .ConfigureAwait(false);
+
+ return new EventResponse(result.Value);
+ }
+
///
public async Task CreateAccessCodeAsync(AccessCodeRequest req)
{
var @event = await this._managements
- .GetEntityIfExistsAsync(ManagementsTablePartitionKey, req.EventId)
+ .GetEntityIfExistsAsync(ManagementsTablePartitionKey, req.EventId)
+ .ConfigureAwait(false)
?? throw new KeyNotFoundException($"Event ID not found: {req.EventId}");
var apiKey = Guid.NewGuid().ToString();
var record = new AccessCodeRecord()
{
PartitionKey = req.EventId,
- RowKey = apiKey,
+ RowKey = req.GitHubAlias,
EventId = req.EventId,
Name = req.Name,
Email = req.Email,
@@ -56,11 +138,9 @@ public async Task CreateAccessCodeAsync(AccessCodeRequest re
DateCreated = DateTimeOffset.UtcNow,
};
- var response = await this._accessCodes.UpsertEntityAsync(record);
- using var reader = new StreamReader(response.ContentStream);
- var payload = await reader.ReadToEndAsync();
- var result = JsonSerializer.Deserialize(payload);
+ await this._accessCodes.UpsertEntityAsync(record).ConfigureAwait(false);
+ var result = await this._accessCodes.GetEntityIfExistsAsync(req.EventId, apiKey).ConfigureAwait(false);
- return new AccessCodeResponse(result);
+ return new AccessCodeResponse(result.Value);
}
}
diff --git a/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json b/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json
index 2538f97b..39b0ae10 100644
--- a/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json
+++ b/src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json
@@ -4,22 +4,18 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
- // },
+ },
- // "AOAI": {
- // "Instances": [
- // {
- // "Endpoint": "{{AOAI_ENDPOINT_0}}",
- // "ApiKey": "{{AOAI_API_KEY_0}}"
- // },
- // {
- // "Endpoint": "{{AOAI_ENDPOINT_1}}",
- // "ApiKey": "{{AOAI_API_KEY_1}}"
- // },
- // {
- // "Endpoint": "{{AOAI_ENDPOINT_2}}",
- // "ApiKey": "{{AOAI_API_KEY_2}}"
- // }
- // ]
+ "ConnectionStrings": {
+ "table": "https://{account_name}.table.core.windows.net/"
+ },
+
+ "AOAI": {
+ "Instances": [
+ {
+ "Endpoint": "https://{account_name}.openai.azure.com/",
+ "ApiKey": "random.apikey.aoai"
+ }
+ ]
}
}
diff --git a/src/AzureOpenAIProxy.ApiApp/appsettings.json b/src/AzureOpenAIProxy.ApiApp/appsettings.json
index f452cc4c..6a9d3d3a 100644
--- a/src/AzureOpenAIProxy.ApiApp/appsettings.json
+++ b/src/AzureOpenAIProxy.ApiApp/appsettings.json
@@ -6,22 +6,18 @@
}
},
- // "AOAI": {
- // "Instances": [
- // {
- // "Endpoint": "{{AOAI_ENDPOINT_0}}",
- // "ApiKey": "{{AOAI_API_KEY_0}}"
- // },
- // {
- // "Endpoint": "{{AOAI_ENDPOINT_1}}",
- // "ApiKey": "{{AOAI_API_KEY_1}}"
- // },
- // {
- // "Endpoint": "{{AOAI_ENDPOINT_2}}",
- // "ApiKey": "{{AOAI_API_KEY_2}}"
- // }
- // ]
- // },
+ "ConnectionStrings": {
+ "table": "https://{account_name}.table.core.windows.net/"
+ },
+
+ "AOAI": {
+ "Instances": [
+ {
+ "Endpoint": "https://{account_name}.openai.azure.com/",
+ "ApiKey": "random.apikey.aoai"
+ }
+ ]
+ },
"AllowedHosts": "*"
}