From 9cde7778e5edb6cc395eda8fa49835983d37b9a4 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Tue, 5 Dec 2023 22:45:37 +0900 Subject: [PATCH] Add endpoints for access code (#14) --- .../ManagementAccessCodeController.cs | 165 ++++++++++++++++++ ...roller.cs => ManagementEventController.cs} | 94 +++------- .../Models/AccessCodeResponse.cs | 4 +- .../Models/AccessCodeResponseCollection.cs | 8 + .../Models/EntityResponseCollection.cs | 39 +++++ .../Models/EventRecord.cs | 5 + .../Models/EventResponse.cs | 2 +- .../Models/EventResponseCollection.cs | 21 +-- .../Services/ManagementAccessCodeService.cs | 116 ++++++++++++ ...ntService.cs => ManagementEventService.cs} | 47 +---- 10 files changed, 366 insertions(+), 135 deletions(-) create mode 100644 src/AzureOpenAIProxy.ApiApp/Controllers/ManagementAccessCodeController.cs rename src/AzureOpenAIProxy.ApiApp/Controllers/{ManagementController.cs => ManagementEventController.cs} (64%) create mode 100644 src/AzureOpenAIProxy.ApiApp/Models/AccessCodeResponseCollection.cs create mode 100644 src/AzureOpenAIProxy.ApiApp/Models/EntityResponseCollection.cs create mode 100644 src/AzureOpenAIProxy.ApiApp/Services/ManagementAccessCodeService.cs rename src/AzureOpenAIProxy.ApiApp/Services/{ManagementService.cs => ManagementEventService.cs} (72%) diff --git a/src/AzureOpenAIProxy.ApiApp/Controllers/ManagementAccessCodeController.cs b/src/AzureOpenAIProxy.ApiApp/Controllers/ManagementAccessCodeController.cs new file mode 100644 index 00000000..2752611b --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Controllers/ManagementAccessCodeController.cs @@ -0,0 +1,165 @@ +using AzureOpenAIProxy.ApiApp.Models; + +using Microsoft.AspNetCore.Mvc; + +namespace AzureOpenAIProxy.ApiApp.Controllers; + +/// +/// This represents the controller entity for management. +/// +public partial class ManagementController +{ + /// + /// Gets the list of access codes by event ID. + /// + /// API key. + /// Event ID. + /// Page number. + /// Page size. + /// Returns the instance. + [HttpGet("events/{eventId}/access-codes", Name = "GetListOfEventAccessCodes")] + public async Task GetEventAccessCodesByEventIdAsync( + [FromHeader(Name = "Authorization")] string apiKey, + [FromRoute] string eventId, + [FromQuery(Name = "page")] int? page = 0, + [FromQuery(Name = "size")] int? size = 20) + { + this._logger.LogInformation("Received a request to get the list of access codes for the given event"); + + 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.GetAccessCodesAsync(eventId, page, size); + + this._logger.LogInformation("Completed the request to get the list of access codes for the given event"); + + return new OkObjectResult(result); + } + catch (Exception ex) + { + this._logger.LogError(ex, "Failed to get the list of access codes for the given event"); + + return new ObjectResult(ex.Message) { StatusCode = StatusCodes.Status500InternalServerError }; + } + } + + /// + /// Creates the access code. + /// + /// API key. + /// Event ID. + /// instance. + /// Returns the instance. + [HttpPost("events/{eventId}/access-codes", Name = "CreateEventAccessCode")] + public async Task CreateEventAccessCodeAsync( + [FromHeader(Name = "Authorization")] string apiKey, + [FromRoute] string eventId, + [FromBody] AccessCodeRequest req) + { + this._logger.LogInformation("Received a request to generate an access code"); + + 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(); + } + + if (req == null) + { + this._logger.LogError("No request payload"); + + return new BadRequestResult(); + } + + try + { + var result = await this._management.CreateAccessCodeAsync(eventId, req); + + this._logger.LogInformation("Completed the request to generate the access code"); + + return new OkObjectResult(result); + } + catch (Exception ex) + { + this._logger.LogError(ex, "Failed to generate the access code"); + + return new ObjectResult(ex.Message) { StatusCode = StatusCodes.Status500InternalServerError }; + } + } + + /// + /// Gets the access code by GitHub alias. + /// + /// API key. + /// Event ID. + /// GitHub alias. + /// Returns the instance. + [HttpGet("events/{eventId}/access-codes/{gitHubAlias}", Name = "GetEventAccessCodeByGitHubAlias")] + public async Task GetEventAccessCodeByGitHubAliasAsync( + [FromHeader(Name = "Authorization")] string apiKey, + [FromRoute] string eventId, + [FromRoute] string gitHubAlias) + { + this._logger.LogInformation("Received a request to get the access code belongs to the given GitHub alias"); + + 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(); + } + + if (string.IsNullOrWhiteSpace(gitHubAlias)) + { + this._logger.LogError("No GitHub alias"); + + return new NotFoundResult(); + } + + try + { + var result = await this._management.GetAccessCodeByGitHubAliasAsync(eventId, gitHubAlias); + + this._logger.LogInformation("Completed the request to get the access code belongs to the given GitHub alias"); + + return new OkObjectResult(result); + } + catch (Exception ex) + { + this._logger.LogError(ex, "Failed to get the access code belongs to the given GitHub alias"); + + return new ObjectResult(ex.Message) { StatusCode = StatusCodes.Status500InternalServerError }; + } + } +} diff --git a/src/AzureOpenAIProxy.ApiApp/Controllers/ManagementController.cs b/src/AzureOpenAIProxy.ApiApp/Controllers/ManagementEventController.cs similarity index 64% rename from src/AzureOpenAIProxy.ApiApp/Controllers/ManagementController.cs rename to src/AzureOpenAIProxy.ApiApp/Controllers/ManagementEventController.cs index 5093a5ac..7060dc2a 100644 --- a/src/AzureOpenAIProxy.ApiApp/Controllers/ManagementController.cs +++ b/src/AzureOpenAIProxy.ApiApp/Controllers/ManagementEventController.cs @@ -13,7 +13,7 @@ namespace AzureOpenAIProxy.ApiApp.Controllers; /// instance. [ApiController] [Route("api/management")] -public class ManagementController( +public partial class ManagementController( [FromKeyedServices("management")] IAuthService auth, IManagementService management, ILogger logger) : ControllerBase @@ -22,12 +22,18 @@ public class ManagementController( private readonly IManagementService _management = management ?? throw new ArgumentNullException(nameof(management)); private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + /// + /// Gets the list of events. + /// + /// API key. + /// Page number. + /// Page size. + /// Returns the instance. [HttpGet("events", Name = "GetListOfEvents")] public async Task GetEventsAsync( [FromHeader(Name = "Authorization")] string apiKey, [FromQuery(Name = "page")] int? page = 0, - [FromQuery(Name = "size")] int? size = 20 - ) + [FromQuery(Name = "size")] int? size = 20) { this._logger.LogInformation("Received a request to get all events"); @@ -55,8 +61,14 @@ public async Task GetEventsAsync( } } + /// + /// Creates the event. + /// + /// API key. + /// instance. + /// Returns the instance. [HttpPost("events", Name = "CreateEvent")] - public async Task CreateEvent( + public async Task CreateEventAsync( [FromHeader(Name = "Authorization")] string apiKey, [FromBody] EventRequest req) { @@ -93,6 +105,12 @@ public async Task CreateEvent( } } + /// + /// Gets the event by ID. + /// + /// API key. + /// Event ID. + /// Returns the instance. [HttpGet("events/{eventId}", Name = "GetEventById")] public async Task GetEventByEventIdAsync( [FromHeader(Name = "Authorization")] string apiKey, @@ -130,72 +148,4 @@ public async Task GetEventByEventIdAsync( return new ObjectResult(ex.Message) { StatusCode = StatusCodes.Status500InternalServerError }; } } - - [HttpGet("events/{eventId}/access-codes", Name = "GetListOfEventAccessCodes")] - public async Task GetAccessCodesByEventIdAsync( - [FromHeader(Name = "Authorization")] string apiKey, - [FromRoute] string eventId) - { - var result = new OkObjectResult("Pong"); - - return await Task.FromResult(result); - } - - [HttpPost("events/{eventId}/access-codes", Name = "CreateEventAccessCode")] - public async Task CreateEvent( - [FromHeader(Name = "Authorization")] string apiKey, - [FromRoute] string eventId, - [FromBody] AccessCodeRequest req) - { - this._logger.LogInformation("Received a request to generate an access code"); - - 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(); - } - - if (req == null) - { - this._logger.LogError("No request payload"); - - return new BadRequestResult(); - } - - try - { - req.EventId = eventId; - var result = await this._management.CreateAccessCodeAsync(req); - - this._logger.LogInformation("Completed the request to generate the access code"); - - return new OkObjectResult(result); - } - catch (Exception ex) - { - this._logger.LogError(ex, "Failed to generate the access code"); - - 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/AccessCodeResponse.cs b/src/AzureOpenAIProxy.ApiApp/Models/AccessCodeResponse.cs index e760a508..8aae7914 100644 --- a/src/AzureOpenAIProxy.ApiApp/Models/AccessCodeResponse.cs +++ b/src/AzureOpenAIProxy.ApiApp/Models/AccessCodeResponse.cs @@ -5,7 +5,7 @@ namespace AzureOpenAIProxy.ApiApp.Models; /// /// This represents the response entity for access code. /// -public class AccessCodeResponse : AccessCodeRequest +public class AccessCodeResponse : AccessCodeRequest, IEntityResponse { /// /// Initializes a new instance of the class. @@ -48,4 +48,4 @@ public AccessCodeResponse(AccessCodeRecord record) /// [JsonPropertyName("maxTokens")] public int? MaxTokens { get; set; } -} \ No newline at end of file +} diff --git a/src/AzureOpenAIProxy.ApiApp/Models/AccessCodeResponseCollection.cs b/src/AzureOpenAIProxy.ApiApp/Models/AccessCodeResponseCollection.cs new file mode 100644 index 00000000..35775a6c --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Models/AccessCodeResponseCollection.cs @@ -0,0 +1,8 @@ +namespace AzureOpenAIProxy.ApiApp.Models; + +/// +/// This represents the response entity collection for access code. +/// +public class AccessCodeResponseCollection : EntityResponseCollection +{ +} diff --git a/src/AzureOpenAIProxy.ApiApp/Models/EntityResponseCollection.cs b/src/AzureOpenAIProxy.ApiApp/Models/EntityResponseCollection.cs new file mode 100644 index 00000000..ff0b91f8 --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Models/EntityResponseCollection.cs @@ -0,0 +1,39 @@ +namespace AzureOpenAIProxy.ApiApp.Models; + +/// +/// This provides interfaces to the response entity classes. +/// +public interface IEntityResponse +{ + /// + /// Gets or sets the entity ID. + /// + string? Id { get; set; } +} + +/// +/// This represents the response entity collection. This MUST be inherited. +/// +/// Type of response +public abstract class EntityResponseCollection where T : IEntityResponse +{ + /// + /// 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; } = []; +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.ApiApp/Models/EventRecord.cs b/src/AzureOpenAIProxy.ApiApp/Models/EventRecord.cs index 92cacd5e..b5e7fa20 100644 --- a/src/AzureOpenAIProxy.ApiApp/Models/EventRecord.cs +++ b/src/AzureOpenAIProxy.ApiApp/Models/EventRecord.cs @@ -54,6 +54,11 @@ public class EventRecord : ITableEntity /// public string? ApiKey { get; set; } + /// + /// Gets or sets the maximum number of tokens for the event. Defaults to 4096. + /// + public int? MaxTokens { get; set; } = 4096; + /// public DateTimeOffset? Timestamp { get; set; } diff --git a/src/AzureOpenAIProxy.ApiApp/Models/EventResponse.cs b/src/AzureOpenAIProxy.ApiApp/Models/EventResponse.cs index ce8bad82..0a292d83 100644 --- a/src/AzureOpenAIProxy.ApiApp/Models/EventResponse.cs +++ b/src/AzureOpenAIProxy.ApiApp/Models/EventResponse.cs @@ -5,7 +5,7 @@ namespace AzureOpenAIProxy.ApiApp.Models; /// /// This represents the response entity for event. /// -public class EventResponse : EventRequest +public class EventResponse : EventRequest, IEntityResponse { /// /// Initializes a new instance of the class. diff --git a/src/AzureOpenAIProxy.ApiApp/Models/EventResponseCollection.cs b/src/AzureOpenAIProxy.ApiApp/Models/EventResponseCollection.cs index 56384486..4ba30a58 100644 --- a/src/AzureOpenAIProxy.ApiApp/Models/EventResponseCollection.cs +++ b/src/AzureOpenAIProxy.ApiApp/Models/EventResponseCollection.cs @@ -3,25 +3,6 @@ /// /// This represents the response entity collection for event. /// -public class EventResponseCollection +public class EventResponseCollection : EntityResponseCollection { - /// - /// 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/Services/ManagementAccessCodeService.cs b/src/AzureOpenAIProxy.ApiApp/Services/ManagementAccessCodeService.cs new file mode 100644 index 00000000..53969184 --- /dev/null +++ b/src/AzureOpenAIProxy.ApiApp/Services/ManagementAccessCodeService.cs @@ -0,0 +1,116 @@ +using AzureOpenAIProxy.ApiApp.Models; + +namespace AzureOpenAIProxy.ApiApp.Services; + +/// +/// This provides interfaces to the class. +/// +public partial interface IManagementService +{ + /// + /// Gets the list of access codes by event ID. + /// + /// Event ID. + /// Page number. + /// Page size. + /// Returns the instance. + Task GetAccessCodesAsync(string eventId, int? page = 0, int? size = 20); + + /// + /// Creates the access code. + /// + /// Event ID. + /// instance. + /// Returns the instance. + Task CreateAccessCodeAsync(string eventId, AccessCodeRequest req); + + /// + /// Gets the access code by GitHub alias. + /// + /// Event ID. + /// GitHub alias. + /// Returns the instance. + Task GetAccessCodeByGitHubAliasAsync(string eventId, string gitHubAlias); +} + +/// +/// This represents the service entity for management. +/// +public partial class ManagementService +{ + /// + public async Task GetAccessCodesAsync(string eventId, int? page = 0, int? size = 20) + { + if (string.IsNullOrWhiteSpace(eventId)) + { + throw new ArgumentNullException(nameof(eventId)); + } + + var results = this._accessCodes.QueryAsync(p => p.PartitionKey + .Equals(eventId, StringComparison.InvariantCultureIgnoreCase)); + var records = new List(); + await foreach (var result in results.AsPages()) + { + records.AddRange(result.Values.Select(p => new AccessCodeResponse(p))); + } + + var skip = page.Value * size.Value; + var take = size.Value; + + var response = new AccessCodeResponseCollection() + { + CurrentPage = page, + PageSize = size, + Total = records.Count, + Items = records.Skip(skip).Take(take).ToList(), + }; + + return response; + } + + /// + public async Task CreateAccessCodeAsync(string eventId, AccessCodeRequest req) + { + var @event = await this._managements + .GetEntityIfExistsAsync( + ManagementsTablePartitionKey, + eventId ?? throw new ArgumentNullException(nameof(eventId))) + .ConfigureAwait(false) + ?? throw new KeyNotFoundException($"Event ID not found: {eventId}"); + + var accessCodeId = Guid.NewGuid().ToString(); + var apiKey = Guid.NewGuid().ToString(); + var record = new AccessCodeRecord() + { + PartitionKey = eventId, + RowKey = req.GitHubAlias, + AccessCodeId = accessCodeId, + EventId = eventId, + Name = req.Name, + Email = req.Email, + GitHubAlias = req.GitHubAlias, + ApiKey = apiKey, + MaxTokens = @event?.Value?.MaxTokens ?? DefaultMaxTokens, + EventDateStart = @event?.Value?.EventDateStart, + EventDateEnd = @event?.Value?.EventDateEnd, + DateCreated = DateTimeOffset.UtcNow, + }; + + await this._accessCodes.UpsertEntityAsync(record).ConfigureAwait(false); + var result = await this._accessCodes.GetEntityIfExistsAsync(eventId, req.GitHubAlias).ConfigureAwait(false); + + return new AccessCodeResponse(result.Value); + } + + /// + public async Task GetAccessCodeByGitHubAliasAsync(string eventId, string gitHubAlias) + { + var result = await this._accessCodes.GetEntityIfExistsAsync( + eventId ?? throw new ArgumentNullException(nameof(eventId)), + gitHubAlias ?? throw new ArgumentNullException(nameof(gitHubAlias))) + .ConfigureAwait(false) + ?? throw new KeyNotFoundException($"Either Event ID: {eventId} or GitHub alias: {gitHubAlias} not found"); + + return new AccessCodeResponse(result.Value); + } +} diff --git a/src/AzureOpenAIProxy.ApiApp/Services/ManagementService.cs b/src/AzureOpenAIProxy.ApiApp/Services/ManagementEventService.cs similarity index 72% rename from src/AzureOpenAIProxy.ApiApp/Services/ManagementService.cs rename to src/AzureOpenAIProxy.ApiApp/Services/ManagementEventService.cs index 1e5923a1..9d62700d 100644 --- a/src/AzureOpenAIProxy.ApiApp/Services/ManagementService.cs +++ b/src/AzureOpenAIProxy.ApiApp/Services/ManagementEventService.cs @@ -7,7 +7,7 @@ namespace AzureOpenAIProxy.ApiApp.Services; /// /// This provides interfaces to the class. /// -public interface IManagementService +public partial interface IManagementService { /// /// Gets the list of events. @@ -25,18 +25,11 @@ public interface IManagementService Task CreateEventAsync(EventRequest req); /// - /// Get the event by ID. + /// Gets the event by ID. /// /// Event ID. /// Returns the instance. Task GetEvenByIdAsync(string eventId); - - /// - /// Creates the access code. - /// - /// instance. - /// Returns the instance. - Task CreateAccessCodeAsync(AccessCodeRequest req); } /// @@ -44,11 +37,12 @@ public interface IManagementService /// /// instance. /// instance. -public class ManagementService(TableServiceClient client, ILogger logger) : IManagementService +public partial class ManagementService(TableServiceClient client, ILogger logger) : IManagementService { private const string AccessCodesTableName = "accesscodes"; private const string ManagementsTableName = "managements"; private const string ManagementsTablePartitionKey = "management"; + private const int DefaultMaxTokens = 4096; private readonly TableClient _accessCodes = client?.GetTableClient(AccessCodesTableName) ?? throw new ArgumentNullException(nameof(client)); private readonly TableClient _managements = client?.GetTableClient(ManagementsTableName) ?? throw new ArgumentNullException(nameof(client)); @@ -96,6 +90,7 @@ public async Task CreateEventAsync(EventRequest req) EventDateStart = req.EventDateStart.Value.ToUniversalTime(), EventDateEnd = req.EventDateEnd.Value.ToUniversalTime(), ApiKey = apiKey, + MaxTokens = DefaultMaxTokens, }; await this._managements.UpsertEntityAsync(record).ConfigureAwait(false); @@ -110,37 +105,9 @@ public async Task GetEvenByIdAsync(string eventId) var result = await this._managements.GetEntityIfExistsAsync( ManagementsTablePartitionKey, eventId ?? throw new ArgumentNullException(nameof(eventId))) - .ConfigureAwait(false); + .ConfigureAwait(false) + ?? throw new KeyNotFoundException($"Event ID not found: {eventId}"); return new EventResponse(result.Value); } - - /// - public async Task CreateAccessCodeAsync(AccessCodeRequest req) - { - var @event = await this._managements - .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 = req.GitHubAlias, - EventId = req.EventId, - Name = req.Name, - Email = req.Email, - GitHubAlias = req.GitHubAlias, - ApiKey = apiKey, - EventDateStart = @event?.Value?.EventDateStart, - EventDateEnd = @event?.Value?.EventDateEnd, - DateCreated = DateTimeOffset.UtcNow, - }; - - await this._accessCodes.UpsertEntityAsync(record).ConfigureAwait(false); - var result = await this._accessCodes.GetEntityIfExistsAsync(req.EventId, apiKey).ConfigureAwait(false); - - return new AccessCodeResponse(result.Value); - } }