2using System.Collections.Generic;
3using System.Globalization;
8using System.Threading.Tasks;
10using Microsoft.AspNetCore.Http;
11using Microsoft.AspNetCore.Mvc;
12using Microsoft.AspNetCore.Mvc.Filters;
13using Microsoft.EntityFrameworkCore;
14using Microsoft.EntityFrameworkCore.Query;
15using Microsoft.Extensions.Logging;
16using Microsoft.Net.Http.Headers;
36 [Produces(MediaTypeNames.Application.Json)]
68 protected ILogger<ApiController>
Logger {
get; }
90 ILogger<ApiController> logger,
93 DatabaseContext = databaseContext ??
throw new ArgumentNullException(nameof(databaseContext));
94 ArgumentNullException.ThrowIfNull(authenticationContextFactory);
95 Logger = logger ??
throw new ArgumentNullException(nameof(logger));
102#pragma warning disable CA1506
105 ArgumentNullException.ThrowIfNull(context);
112 await Unauthorized().ExecuteResultAsync(context);
124 HttpStatusCode.UpgradeRequired,
126 .ExecuteResultAsync(context);
130 var errorCase = await
ValidateRequest(context.HttpContext.RequestAborted);
131 if (errorCase !=
null)
133 await errorCase.ExecuteResultAsync(context);
142 .ExecuteResultAsync(context);
147 if (ModelState?.IsValid ==
false)
149 var errorMessages = ModelState
150 .SelectMany(x => x.Value.Errors)
151 .Select(x => x.ErrorMessage)
157 .Where(x => !x.EndsWith(
" field is required.", StringComparison.Ordinal));
159 if (errorMessages.Any())
164 AdditionalData = String.Join(Environment.NewLine, errorMessages),
166 .ExecuteResultAsync(context);
183 var isGet = HttpMethods.IsGet(Request.Method);
184 if (!(isGet && Request.Path.StartsWithSegments(
Routes.
Jobs, StringComparison.OrdinalIgnoreCase)))
189 "Starting API request: Version: {clientApiVersion}. {userAgentHeaderName}: {clientUserAgent}",
191 HeaderNames.UserAgent,
194 else if (Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgents))
196 "Starting unauthorized API request. {userAgentHeaderName}: {allUserAgents}",
197 HeaderNames.UserAgent,
201 "Starting unauthorized API request. No {userAgentHeaderName}!",
202 HeaderNames.UserAgent);
203 await base.OnActionExecutionAsync(context, next);
206#pragma warning restore CA1506
221 Logger.LogTrace(ex,
"System identities not implemented!");
237 protected ObjectResult
Created(
object payload) =>
StatusCode((
int)HttpStatusCode.Created, payload);
244 protected ObjectResult
RateLimit(RateLimitExceededException rateLimitException)
246 ArgumentNullException.ThrowIfNull(rateLimitException);
248 Logger.LogWarning(rateLimitException,
"Exceeded GitHub rate limit!");
250 var secondsString = Math.Ceiling(rateLimitException.GetRetryAfterTimeSpan().TotalSeconds).ToString(CultureInfo.InvariantCulture);
251 Response.Headers.Add(HeaderNames.RetryAfter, secondsString);
260 protected virtual Task<IActionResult>
ValidateRequest(CancellationToken cancellationToken)
261 => Task.FromResult<IActionResult>(
null);
273 _ =
new ApiHeaders(Request.GetTypedHeaders(), ignoreMissingAuth);
274 throw new InvalidOperationException(
"Expected a header parse exception!");
278 headersException = ex;
283 AdditionalData = headersException.Message,
287 return this.
StatusCode(HttpStatusCode.NotAcceptable, errorMessage);
289 return BadRequest(errorMessage);
304 Func<TModel, Task> resultTransformer,
307 CancellationToken cancellationToken) => PaginatedImpl(
327 Func<TApiModel, Task> resultTransformer,
330 CancellationToken cancellationToken)
352 Func<TResultModel, Task> resultTransformer,
355 CancellationToken cancellationToken)
357 ArgumentNullException.ThrowIfNull(queryGenerator);
359 if (pageQuery <= 0 || pageSizeQuery <= 0)
366 AdditionalData = $
"Maximum page size: {MaximumPageSize}",
369 var page = pageQuery ?? 1;
371 var paginationResult = await queryGenerator();
372 if (paginationResult.EarlyOut !=
null)
373 return paginationResult.EarlyOut;
375 var queriedResults = paginationResult
377 .Skip((page - 1) * pageSize)
381 List<TModel> pagedResults;
382 if (queriedResults.Provider is IAsyncQueryProvider)
384 totalResults = await paginationResult.Results.CountAsync(cancellationToken);
385 pagedResults = await queriedResults
386 .ToListAsync(cancellationToken);
390 totalResults = paginationResult.Results.Count();
391 pagedResults = queriedResults.ToList();
394 ICollection<TResultModel> finalResults;
395 if (typeof(TModel) == typeof(TResultModel))
396 finalResults = (List<TResultModel>)(
object)pagedResults;
398 finalResults = pagedResults
400 .Select(x => x.ToApi())
403 if (resultTransformer !=
null)
404 foreach (var finalResult
in finalResults)
405 await resultTransformer(finalResult);
407 var carryTheOne = totalResults % pageSize != 0
413 Content = finalResults,
415 TotalPages = (ushort)(totalResults / pageSize) + carryTheOne,
416 TotalItems = totalResults,
virtual ? long Id
The ID of the entity.
Metadata about a server instance.
Represents an error message returned by the server.
Represents a paginated set of models.
Routes to a server actions.
const string Jobs
The jobs controller.
Base Controller for API functions.
ApiHeaders ApiHeaders
The Api.ApiHeaders for the operation.
virtual Task< IActionResult > ValidateRequest(CancellationToken cancellationToken)
Performs validation a request.
const ushort MaximumPageSize
Maximum size of Paginated<TModel> results.
IActionResult HeadersIssue(bool ignoreMissingAuth)
Response for missing/Invalid headers.
new NotFoundObjectResult NotFound()
Generic 404 response.
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger< ApiController > logger, bool requireHeaders)
Initializes a new instance of the ApiController class.
ObjectResult RequiresPosixSystemIdentity(NotImplementedException ex)
Generic 501 response.
readonly bool requireHeaders
If ApiHeaders are required.
StatusCodeResult StatusCode(HttpStatusCode statusCode)
Strongly type calls to ControllerBase.StatusCode(int).
async Task< IActionResult > PaginatedImpl< TModel, TResultModel >(Func< Task< PaginatableResult< TModel > > > queryGenerator, Func< TResultModel, Task > resultTransformer, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken)
Generates a paginated response.
const ushort DefaultPageSize
Default size of Paginated<TModel> results.
Task< IActionResult > Paginated< TModel >(Func< Task< PaginatableResult< TModel > > > queryGenerator, Func< TModel, Task > resultTransformer, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken)
Generates a paginated response.
Task< IActionResult > Paginated< TModel, TApiModel >(Func< Task< PaginatableResult< TModel > > > queryGenerator, Func< TApiModel, Task > resultTransformer, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken)
Generates a paginated response.
ObjectResult RateLimit(RateLimitExceededException rateLimitException)
429 response for a given rateLimitException .
ObjectResult Created(object payload)
Generic 201 response with a given payload .
ILogger< ApiController > Logger
The ILogger for the ApiController.
override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
Helper for returning paginated models.
Backend abstract implementation of IDatabaseContext.
Represents an Api.Models.Instance in the database.
Instance Instance
The Models.Instance the InstancePermissionSet belongs to.
User User
The authenticated user.
InstancePermissionSet InstancePermissionSet
The User's effective Models.InstancePermissionSet if applicable.
Helpers for manipulating the Serilog.Context.LogContext.
const string InstanceIdContextProperty
The Serilog.Context.LogContext property name for Models.Instance Api.Models.EntityId....
const string UserIdContextProperty
The Serilog.Context.LogContext property name for Models.Instance Api.Models.EntityId....
const string RequestPathContextProperty
The Serilog.Context.LogContext property name for Models.User Api.Models.EntityId.Ids.
For creating and accessing authentication contexts.
IAuthenticationContext CurrentAuthenticationContext
The IAuthenticationContext the IAuthenticationContextFactory created.
Represents the currently authenticated Models.User.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
HeaderTypes
Types of individual ApiHeaders.