Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ X-Api-Version: 20240101

Wherever possible you should call the latest version.

You can view the API specifications for each version by visiting `/swagger` (see [Environments](#environments) below for the base addresses).
You can view the API specifications for each version by visiting `/docs` (see [Environments](#environments) below for the base addresses).

An API key is required for calling the API; speak to one of the developers to get one. The key must be passed in an `Authorization` header e.g.
```
Expand Down
4 changes: 2 additions & 2 deletions TeachingRecordSystem/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="10.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.1.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.3" />
Expand Down Expand Up @@ -87,6 +88,7 @@
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="RedisRateLimiting.AspNetCore" Version="1.2.0" />
<PackageVersion Include="Respawn" Version="7.0.0" />
<PackageVersion Include="Scalar.AspNetCore.Microsoft" Version="2.12.48" />
<PackageVersion Include="Scrutor" Version="7.0.0" />
<PackageVersion Include="Sentry.AspNetCore" Version="6.1.0" />
<PackageVersion Include="Sentry.Extensions.Logging" Version="6.1.0" />
Expand All @@ -99,8 +101,6 @@
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="SerilogTimings" Version="3.1.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.6.2" />
<PackageVersion Include="System.CommandLine" Version="2.0.3" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
<PackageVersion Include="System.Net.Http.Json" Version="10.0.1" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

namespace TeachingRecordSystem.Api.Infrastructure.OpenApi;

internal class AddMinorVersionHeaderTransformer(string minorVersion) : IOpenApiDocumentTransformer
{
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
{
var headerValueSchemaName = "VersionHeader";

document.Components ??= new OpenApiComponents();
document.Components.Schemas ??= new Dictionary<string, IOpenApiSchema>();
document.Components.Schemas.Add(headerValueSchemaName, new OpenApiSchema
{
Type = JsonSchemaType.String,
Const = minorVersion
});

foreach (var (_, path) in document.Paths)
{
foreach (var (_, op) in path.Operations!)
{
op.Parameters ??= new List<IOpenApiParameter>();
op.Parameters.Add(new OpenApiParameter()
{
Name = VersionRegistry.MinorVersionHeaderName,
Required = true,
In = ParameterLocation.Header,
Schema = new OpenApiSchema { Id = headerValueSchemaName }
});
}
}

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
using TeachingRecordSystem.Api.Infrastructure.Security;

namespace TeachingRecordSystem.Api.Infrastructure.OpenApi;

public class AddSecuritySchemeOperationFilter : IOperationFilter
internal class AddSecurityRequirementsTransformer : IOpenApiOperationTransformer
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
var actionDescriptor = context.ApiDescription.ActionDescriptor;
var actionDescriptor = context.Description.ActionDescriptor;

var authorizeAttributes = actionDescriptor.EndpointMetadata.OfType<AuthorizeAttribute>().ToArray();

if (authorizeAttributes.Length != 1)
{
throw new NotSupportedException("Cannot derive security scheme for operation.");
Expand All @@ -21,15 +20,12 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
var authorizationPolicy = authorizeAttributes.Single().Policy;
OpenApiSecurityRequirement? requirement;

if (authorizationPolicy == AuthorizationPolicies.ApiKey)
if (authorizationPolicy is AuthorizationPolicies.ApiKey)
{
requirement = new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = SecuritySchemes.ApiKey }
},
new OpenApiSecuritySchemeReference(SecuritySchemes.ApiKey),
[]
}
};
Expand All @@ -39,11 +35,8 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
requirement = new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = SecuritySchemes.GetAnIdentityAccessToken }
},
[]
new OpenApiSecuritySchemeReference(SecuritySchemes.GetAnIdentityAccessToken),
["trn"] // The OAuth scopes required
}
};
}
Expand All @@ -54,5 +47,7 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)

operation.Security ??= new List<OpenApiSecurityRequirement>();
operation.Security.Add(requirement);

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;

namespace TeachingRecordSystem.Api.Infrastructure.OpenApi;

internal class AddSecuritySchemesTransformer(IConfiguration configuration) : IOpenApiDocumentTransformer
{
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
{
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();

document.Components.SecuritySchemes.Add(SecuritySchemes.ApiKey, new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = "Authorization",
Scheme = "Bearer",
Type = SecuritySchemeType.Http
});

document.Components.SecuritySchemes.Add(SecuritySchemes.GetAnIdentityAccessToken, new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Scheme = "Bearer",
Type = SecuritySchemeType.OpenIdConnect,
OpenIdConnectUrl = new Uri(configuration.GetRequiredValue("GetAnIdentity:BaseAddress") + ".well-known/openid-configuration")
});

return Task.CompletedTask;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
using TeachingRecordSystem.Core.ApiSchema.V3;

namespace TeachingRecordSystem.Api.Infrastructure.OpenApi;

internal class AddWebhookMessagesTransformer(string minorVersion) : IOpenApiDocumentTransformer
{
public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
{
var messagesNamespace = $"TeachingRecordSystem.Core.ApiSchema.V3.V{minorVersion}.WebhookData";

var messageTypes = typeof(IWebhookMessageData).Assembly.GetTypes()
.Where(t => t.Namespace == messagesNamespace && t.IsAssignableTo(typeof(IWebhookMessageData)));

document.Webhooks ??= new Dictionary<string, IOpenApiPathItem>();

foreach (var messageType in messageTypes)
{
var cloudEventName = messageType.GetProperty(nameof(IWebhookMessageData.CloudEventType))?.GetValue(null) as string ??
throw new InvalidOperationException($"Webhook message type {messageType.FullName} does not have a valid CloudEventType property.");

var schema = await context.GetOrCreateSchemaAsync(messageType, cancellationToken: cancellationToken);

var path = new OpenApiPathItem
{
Operations = new Dictionary<HttpMethod, OpenApiOperation>
{
[HttpMethod.Post] = new()
{
RequestBody = new OpenApiRequestBody
{
Content = new Dictionary<string, OpenApiMediaType>
{
["application/json"] = new()
{
Schema = schema
}
}
},
Responses = new OpenApiResponses
{
["200"] = new OpenApiResponse
{
Description = "Success"
}
}
}
}
};

document.Webhooks.Add(cloudEventName, path);
}
}
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading