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": "*" }