mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM-25182] Improve swagger OperationIDs: Part 1 (#6229)
* Improve swagger OperationIDs: Part 1 * Fix tests and fmt * Improve docs and add more tests * Fmt * Improve Swagger OperationIDs for Auth * Fix review feedback * Use generic getcustomattributes * Format * replace swaggerexclude by split+obsolete * Format * Some remaining excludes
This commit is contained in:
@@ -11,9 +11,18 @@ dotnet tool restore
|
||||
Set-Location "./src/Identity"
|
||||
dotnet build
|
||||
dotnet swagger tofile --output "../../identity.json" --host "https://identity.bitwarden.com" "./bin/Debug/net8.0/Identity.dll" "v1"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
# Api internal & public
|
||||
Set-Location "../../src/Api"
|
||||
dotnet build
|
||||
dotnet swagger tofile --output "../../api.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "internal"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
dotnet swagger tofile --output "../../api.public.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "public"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
@@ -344,7 +344,6 @@ public class AccountsController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("profile")]
|
||||
[HttpPost("profile")]
|
||||
public async Task<ProfileResponseModel> PutProfile([FromBody] UpdateProfileRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
@@ -363,8 +362,14 @@ public class AccountsController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("profile")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /profile instead.")]
|
||||
public async Task<ProfileResponseModel> PostProfile([FromBody] UpdateProfileRequestModel model)
|
||||
{
|
||||
return await PutProfile(model);
|
||||
}
|
||||
|
||||
[HttpPut("avatar")]
|
||||
[HttpPost("avatar")]
|
||||
public async Task<ProfileResponseModel> PutAvatar([FromBody] UpdateAvatarRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
@@ -382,6 +387,13 @@ public class AccountsController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("avatar")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /avatar instead.")]
|
||||
public async Task<ProfileResponseModel> PostAvatar([FromBody] UpdateAvatarRequestModel model)
|
||||
{
|
||||
return await PutAvatar(model);
|
||||
}
|
||||
|
||||
[HttpGet("revision-date")]
|
||||
public async Task<long?> GetAccountRevisionDate()
|
||||
{
|
||||
@@ -430,7 +442,6 @@ public class AccountsController : Controller
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[HttpPost("delete")]
|
||||
public async Task Delete([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
@@ -467,6 +478,13 @@ public class AccountsController : Controller
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
[HttpPost("delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE / instead.")]
|
||||
public async Task PostDelete([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
await Delete(model);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("delete-recover")]
|
||||
public async Task PostDeleteRecover([FromBody] DeleteRecoverRequestModel model)
|
||||
@@ -638,7 +656,6 @@ public class AccountsController : Controller
|
||||
await _twoFactorEmailService.SendNewDeviceVerificationEmailAsync(user);
|
||||
}
|
||||
|
||||
[HttpPost("verify-devices")]
|
||||
[HttpPut("verify-devices")]
|
||||
public async Task SetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request)
|
||||
{
|
||||
@@ -654,6 +671,13 @@ public class AccountsController : Controller
|
||||
await _userService.SaveUserAsync(user);
|
||||
}
|
||||
|
||||
[HttpPost("verify-devices")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /verify-devices instead.")]
|
||||
public async Task PostSetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request)
|
||||
{
|
||||
await SetUserVerifyDevicesAsync(request);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<Guid>> GetOrganizationIdsClaimingUserAsync(Guid userId)
|
||||
{
|
||||
var organizationsClaimingUser = await _userService.GetOrganizationsClaimingUserAsync(userId);
|
||||
|
||||
@@ -31,7 +31,7 @@ public class AuthRequestsController(
|
||||
private readonly IAuthRequestService _authRequestService = authRequestService;
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<AuthRequestResponseModel>> Get()
|
||||
public async Task<ListResponseModel<AuthRequestResponseModel>> GetAll()
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var authRequests = await _authRequestRepository.GetManyByUserIdAsync(userId);
|
||||
|
||||
@@ -79,7 +79,6 @@ public class EmergencyAccessController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task Put(Guid id, [FromBody] EmergencyAccessUpdateRequestModel model)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
@@ -92,14 +91,27 @@ public class EmergencyAccessController : Controller
|
||||
await _emergencyAccessService.SaveAsync(model.ToEmergencyAccess(emergencyAccess), user);
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /{id} instead.")]
|
||||
public async Task Post(Guid id, [FromBody] EmergencyAccessUpdateRequestModel model)
|
||||
{
|
||||
await Put(id, model);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(Guid id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
await _emergencyAccessService.DeleteAsync(id, userId.Value);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE /{id} instead.")]
|
||||
public async Task PostDelete(Guid id)
|
||||
{
|
||||
await Delete(id);
|
||||
}
|
||||
|
||||
[HttpPost("invite")]
|
||||
public async Task Invite([FromBody] EmergencyAccessInviteRequestModel model)
|
||||
{
|
||||
@@ -136,7 +148,7 @@ public class EmergencyAccessController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("{id}/approve")]
|
||||
public async Task Accept(Guid id)
|
||||
public async Task Approve(Guid id)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
await _emergencyAccessService.ApproveAsync(id, user);
|
||||
|
||||
@@ -110,7 +110,6 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("authenticator")]
|
||||
[HttpPost("authenticator")]
|
||||
public async Task<TwoFactorAuthenticatorResponseModel> PutAuthenticator(
|
||||
[FromBody] UpdateTwoFactorAuthenticatorRequestModel model)
|
||||
{
|
||||
@@ -133,6 +132,14 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("authenticator")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /authenticator instead.")]
|
||||
public async Task<TwoFactorAuthenticatorResponseModel> PostAuthenticator(
|
||||
[FromBody] UpdateTwoFactorAuthenticatorRequestModel model)
|
||||
{
|
||||
return await PutAuthenticator(model);
|
||||
}
|
||||
|
||||
[HttpDelete("authenticator")]
|
||||
public async Task<TwoFactorProviderResponseModel> DisableAuthenticator(
|
||||
[FromBody] TwoFactorAuthenticatorDisableRequestModel model)
|
||||
@@ -157,7 +164,6 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("yubikey")]
|
||||
[HttpPost("yubikey")]
|
||||
public async Task<TwoFactorYubiKeyResponseModel> PutYubiKey([FromBody] UpdateTwoFactorYubicoOtpRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, true);
|
||||
@@ -174,6 +180,13 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("yubikey")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /yubikey instead.")]
|
||||
public async Task<TwoFactorYubiKeyResponseModel> PostYubiKey([FromBody] UpdateTwoFactorYubicoOtpRequestModel model)
|
||||
{
|
||||
return await PutYubiKey(model);
|
||||
}
|
||||
|
||||
[HttpPost("get-duo")]
|
||||
public async Task<TwoFactorDuoResponseModel> GetDuo([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
@@ -183,7 +196,6 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("duo")]
|
||||
[HttpPost("duo")]
|
||||
public async Task<TwoFactorDuoResponseModel> PutDuo([FromBody] UpdateTwoFactorDuoRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, true);
|
||||
@@ -199,6 +211,13 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("duo")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /duo instead.")]
|
||||
public async Task<TwoFactorDuoResponseModel> PostDuo([FromBody] UpdateTwoFactorDuoRequestModel model)
|
||||
{
|
||||
return await PutDuo(model);
|
||||
}
|
||||
|
||||
[HttpPost("~/organizations/{id}/two-factor/get-duo")]
|
||||
public async Task<TwoFactorDuoResponseModel> GetOrganizationDuo(string id,
|
||||
[FromBody] SecretVerificationRequestModel model)
|
||||
@@ -217,7 +236,6 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("~/organizations/{id}/two-factor/duo")]
|
||||
[HttpPost("~/organizations/{id}/two-factor/duo")]
|
||||
public async Task<TwoFactorDuoResponseModel> PutOrganizationDuo(string id,
|
||||
[FromBody] UpdateTwoFactorDuoRequestModel model)
|
||||
{
|
||||
@@ -243,6 +261,14 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("~/organizations/{id}/two-factor/duo")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /organizations/{id}/two-factor/duo instead.")]
|
||||
public async Task<TwoFactorDuoResponseModel> PostOrganizationDuo(string id,
|
||||
[FromBody] UpdateTwoFactorDuoRequestModel model)
|
||||
{
|
||||
return await PutOrganizationDuo(id, model);
|
||||
}
|
||||
|
||||
[HttpPost("get-webauthn")]
|
||||
public async Task<TwoFactorWebAuthnResponseModel> GetWebAuthn([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
@@ -261,7 +287,6 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("webauthn")]
|
||||
[HttpPost("webauthn")]
|
||||
public async Task<TwoFactorWebAuthnResponseModel> PutWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, false);
|
||||
@@ -277,6 +302,13 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("webauthn")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /webauthn instead.")]
|
||||
public async Task<TwoFactorWebAuthnResponseModel> PostWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model)
|
||||
{
|
||||
return await PutWebAuthn(model);
|
||||
}
|
||||
|
||||
[HttpDelete("webauthn")]
|
||||
public async Task<TwoFactorWebAuthnResponseModel> DeleteWebAuthn(
|
||||
[FromBody] TwoFactorWebAuthnDeleteRequestModel model)
|
||||
@@ -349,7 +381,6 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("email")]
|
||||
[HttpPost("email")]
|
||||
public async Task<TwoFactorEmailResponseModel> PutEmail([FromBody] UpdateTwoFactorEmailRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, false);
|
||||
@@ -367,8 +398,14 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("email")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /email instead.")]
|
||||
public async Task<TwoFactorEmailResponseModel> PostEmail([FromBody] UpdateTwoFactorEmailRequestModel model)
|
||||
{
|
||||
return await PutEmail(model);
|
||||
}
|
||||
|
||||
[HttpPut("disable")]
|
||||
[HttpPost("disable")]
|
||||
public async Task<TwoFactorProviderResponseModel> PutDisable([FromBody] TwoFactorProviderRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, false);
|
||||
@@ -377,8 +414,14 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("disable")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /disable instead.")]
|
||||
public async Task<TwoFactorProviderResponseModel> PostDisable([FromBody] TwoFactorProviderRequestModel model)
|
||||
{
|
||||
return await PutDisable(model);
|
||||
}
|
||||
|
||||
[HttpPut("~/organizations/{id}/two-factor/disable")]
|
||||
[HttpPost("~/organizations/{id}/two-factor/disable")]
|
||||
public async Task<TwoFactorProviderResponseModel> PutOrganizationDisable(string id,
|
||||
[FromBody] TwoFactorProviderRequestModel model)
|
||||
{
|
||||
@@ -401,6 +444,14 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("~/organizations/{id}/two-factor/disable")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /organizations/{id}/two-factor/disable instead.")]
|
||||
public async Task<TwoFactorProviderResponseModel> PostOrganizationDisable(string id,
|
||||
[FromBody] TwoFactorProviderRequestModel model)
|
||||
{
|
||||
return await PutOrganizationDisable(id, model);
|
||||
}
|
||||
|
||||
[HttpPost("get-recover")]
|
||||
public async Task<TwoFactorRecoverResponseModel> GetRecover([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
|
||||
@@ -102,7 +102,7 @@ public class CollectionsController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<CollectionResponseModel>> Get(Guid orgId)
|
||||
public async Task<ListResponseModel<CollectionResponseModel>> GetAll(Guid orgId)
|
||||
{
|
||||
IEnumerable<Collection> orgCollections;
|
||||
|
||||
@@ -173,7 +173,6 @@ public class CollectionsController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task<CollectionResponseModel> Put(Guid orgId, Guid id, [FromBody] UpdateCollectionRequestModel model)
|
||||
{
|
||||
var collection = await _collectionRepository.GetByIdAsync(id);
|
||||
@@ -198,6 +197,13 @@ public class CollectionsController : Controller
|
||||
return new CollectionAccessDetailsResponseModel(collectionWithPermissions);
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /{id} instead.")]
|
||||
public async Task<CollectionResponseModel> Post(Guid orgId, Guid id, [FromBody] UpdateCollectionRequestModel model)
|
||||
{
|
||||
return await Put(orgId, id, model);
|
||||
}
|
||||
|
||||
[HttpPost("bulk-access")]
|
||||
public async Task PostBulkCollectionAccess(Guid orgId, [FromBody] BulkCollectionAccessRequestModel model)
|
||||
{
|
||||
@@ -222,7 +228,6 @@ public class CollectionsController : Controller
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(Guid orgId, Guid id)
|
||||
{
|
||||
var collection = await _collectionRepository.GetByIdAsync(id);
|
||||
@@ -235,8 +240,14 @@ public class CollectionsController : Controller
|
||||
await _deleteCollectionCommand.DeleteAsync(collection);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE /{id} instead.")]
|
||||
public async Task PostDelete(Guid orgId, Guid id)
|
||||
{
|
||||
await Delete(orgId, id);
|
||||
}
|
||||
|
||||
[HttpDelete("")]
|
||||
[HttpPost("delete")]
|
||||
public async Task DeleteMany(Guid orgId, [FromBody] CollectionBulkDeleteRequestModel model)
|
||||
{
|
||||
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Ids);
|
||||
@@ -248,4 +259,11 @@ public class CollectionsController : Controller
|
||||
|
||||
await _deleteCollectionCommand.DeleteManyAsync(collections);
|
||||
}
|
||||
|
||||
[HttpPost("delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE / instead.")]
|
||||
public async Task PostDeleteMany(Guid orgId, [FromBody] CollectionBulkDeleteRequestModel model)
|
||||
{
|
||||
await DeleteMany(orgId, model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ public class DevicesController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<DeviceAuthRequestResponseModel>> Get()
|
||||
public async Task<ListResponseModel<DeviceAuthRequestResponseModel>> GetAll()
|
||||
{
|
||||
var devicesWithPendingAuthData = await _deviceRepository.GetManyByUserIdWithDeviceAuth(_userService.GetProperUserId(User).Value);
|
||||
|
||||
@@ -99,7 +99,6 @@ public class DevicesController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task<DeviceResponseModel> Put(string id, [FromBody] DeviceRequestModel model)
|
||||
{
|
||||
var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value);
|
||||
@@ -114,8 +113,14 @@ public class DevicesController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /{id} instead.")]
|
||||
public async Task<DeviceResponseModel> Post(string id, [FromBody] DeviceRequestModel model)
|
||||
{
|
||||
return await Put(id, model);
|
||||
}
|
||||
|
||||
[HttpPut("{identifier}/keys")]
|
||||
[HttpPost("{identifier}/keys")]
|
||||
public async Task<DeviceResponseModel> PutKeys(string identifier, [FromBody] DeviceKeysRequestModel model)
|
||||
{
|
||||
var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value);
|
||||
@@ -130,6 +135,13 @@ public class DevicesController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("{identifier}/keys")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /{identifier}/keys instead.")]
|
||||
public async Task<DeviceResponseModel> PostKeys(string identifier, [FromBody] DeviceKeysRequestModel model)
|
||||
{
|
||||
return await PutKeys(identifier, model);
|
||||
}
|
||||
|
||||
[HttpPost("{identifier}/retrieve-keys")]
|
||||
[Obsolete("This endpoint is deprecated. The keys are on the regular device GET endpoints now.")]
|
||||
public async Task<ProtectedDeviceResponseModel> GetDeviceKeys(string identifier)
|
||||
@@ -187,7 +199,6 @@ public class DevicesController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("identifier/{identifier}/token")]
|
||||
[HttpPost("identifier/{identifier}/token")]
|
||||
public async Task PutToken(string identifier, [FromBody] DeviceTokenRequestModel model)
|
||||
{
|
||||
var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value);
|
||||
@@ -199,8 +210,14 @@ public class DevicesController : Controller
|
||||
await _deviceService.SaveAsync(model.ToDevice(device));
|
||||
}
|
||||
|
||||
[HttpPost("identifier/{identifier}/token")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /identifier/{identifier}/token instead.")]
|
||||
public async Task PostToken(string identifier, [FromBody] DeviceTokenRequestModel model)
|
||||
{
|
||||
await PutToken(identifier, model);
|
||||
}
|
||||
|
||||
[HttpPut("identifier/{identifier}/web-push-auth")]
|
||||
[HttpPost("identifier/{identifier}/web-push-auth")]
|
||||
public async Task PutWebPushAuth(string identifier, [FromBody] WebPushAuthRequestModel model)
|
||||
{
|
||||
var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value);
|
||||
@@ -216,9 +233,15 @@ public class DevicesController : Controller
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPost("identifier/{identifier}/web-push-auth")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /identifier/{identifier}/web-push-auth instead.")]
|
||||
public async Task PostWebPushAuth(string identifier, [FromBody] WebPushAuthRequestModel model)
|
||||
{
|
||||
await PutWebPushAuth(identifier, model);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPut("identifier/{identifier}/clear-token")]
|
||||
[HttpPost("identifier/{identifier}/clear-token")]
|
||||
public async Task PutClearToken(string identifier)
|
||||
{
|
||||
var device = await _deviceRepository.GetByIdentifierAsync(identifier);
|
||||
@@ -230,8 +253,15 @@ public class DevicesController : Controller
|
||||
await _deviceService.ClearTokenAsync(device);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("identifier/{identifier}/clear-token")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /identifier/{identifier}/clear-token instead.")]
|
||||
public async Task PostClearToken(string identifier)
|
||||
{
|
||||
await PutClearToken(identifier);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/deactivate")]
|
||||
public async Task Deactivate(string id)
|
||||
{
|
||||
var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value);
|
||||
@@ -243,17 +273,24 @@ public class DevicesController : Controller
|
||||
await _deviceService.DeactivateAsync(device);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/deactivate")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE /{id} instead.")]
|
||||
public async Task PostDeactivate(string id)
|
||||
{
|
||||
await Deactivate(id);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("knowndevice")]
|
||||
public async Task<bool> GetByIdentifierQuery(
|
||||
[Required][FromHeader(Name = "X-Request-Email")] string Email,
|
||||
[Required][FromHeader(Name = "X-Device-Identifier")] string DeviceIdentifier)
|
||||
=> await GetByIdentifier(CoreHelpers.Base64UrlDecodeString(Email), DeviceIdentifier);
|
||||
=> await GetByEmailAndIdentifier(CoreHelpers.Base64UrlDecodeString(Email), DeviceIdentifier);
|
||||
|
||||
[Obsolete("Path is deprecated due to encoding issues, use /knowndevice instead.")]
|
||||
[AllowAnonymous]
|
||||
[HttpGet("knowndevice/{email}/{identifier}")]
|
||||
public async Task<bool> GetByIdentifier(string email, string identifier)
|
||||
public async Task<bool> GetByEmailAndIdentifier(string email, string identifier)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
|
||||
@@ -6,12 +6,18 @@ namespace Bit.Api.Controllers;
|
||||
public class InfoController : Controller
|
||||
{
|
||||
[HttpGet("~/alive")]
|
||||
[HttpGet("~/now")]
|
||||
public DateTime GetAlive()
|
||||
{
|
||||
return DateTime.UtcNow;
|
||||
}
|
||||
|
||||
[HttpGet("~/now")]
|
||||
[Obsolete("This endpoint is deprecated. Use GET /alive instead.")]
|
||||
public DateTime GetNow()
|
||||
{
|
||||
return GetAlive();
|
||||
}
|
||||
|
||||
[HttpGet("~/version")]
|
||||
public JsonResult GetVersion()
|
||||
{
|
||||
|
||||
@@ -32,7 +32,6 @@ public class SettingsController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("domains")]
|
||||
[HttpPost("domains")]
|
||||
public async Task<DomainsResponseModel> PutDomains([FromBody] UpdateDomainsRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
@@ -46,4 +45,11 @@ public class SettingsController : Controller
|
||||
var response = new DomainsResponseModel(user);
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("domains")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /domains instead.")]
|
||||
public async Task<DomainsResponseModel> PostDomains([FromBody] UpdateDomainsRequestModel model)
|
||||
{
|
||||
return await PutDomains(model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,15 +82,7 @@ public static class ServiceCollectionExtensions
|
||||
config.DescribeAllParametersInCamelCase();
|
||||
// config.UseReferencedDefinitionsForEnums();
|
||||
|
||||
config.SchemaFilter<EnumSchemaFilter>();
|
||||
config.SchemaFilter<EncryptedStringSchemaFilter>();
|
||||
|
||||
// These two filters require debug symbols/git, so only add them in development mode
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
config.DocumentFilter<GitCommitDocumentFilter>();
|
||||
config.OperationFilter<SourceFileLineOperationFilter>();
|
||||
}
|
||||
config.InitializeSwaggerFilters(environment);
|
||||
|
||||
var apiFilePath = Path.Combine(AppContext.BaseDirectory, "Api.xml");
|
||||
config.IncludeXmlComments(apiFilePath, true);
|
||||
|
||||
@@ -6,12 +6,18 @@ namespace Bit.Identity.Controllers;
|
||||
public class InfoController : Controller
|
||||
{
|
||||
[HttpGet("~/alive")]
|
||||
[HttpGet("~/now")]
|
||||
public DateTime GetAlive()
|
||||
{
|
||||
return DateTime.UtcNow;
|
||||
}
|
||||
|
||||
[HttpGet("~/now")]
|
||||
[Obsolete("This endpoint is deprecated. Use GET /alive instead.")]
|
||||
public DateTime GetNow()
|
||||
{
|
||||
return GetAlive();
|
||||
}
|
||||
|
||||
[HttpGet("~/version")]
|
||||
public JsonResult GetVersion()
|
||||
{
|
||||
|
||||
@@ -66,15 +66,7 @@ public class Startup
|
||||
|
||||
services.AddSwaggerGen(config =>
|
||||
{
|
||||
config.SchemaFilter<EnumSchemaFilter>();
|
||||
config.SchemaFilter<EncryptedStringSchemaFilter>();
|
||||
|
||||
// These two filters require debug symbols/git, so only add them in development mode
|
||||
if (Environment.IsDevelopment())
|
||||
{
|
||||
config.DocumentFilter<GitCommitDocumentFilter>();
|
||||
config.OperationFilter<SourceFileLineOperationFilter>();
|
||||
}
|
||||
config.InitializeSwaggerFilters(Environment);
|
||||
|
||||
config.SwaggerDoc("v1", new OpenApiInfo { Title = "Bitwarden Identity", Version = "v1" });
|
||||
});
|
||||
|
||||
25
src/SharedWeb/Swagger/ActionNameOperationFilter.cs
Normal file
25
src/SharedWeb/Swagger/ActionNameOperationFilter.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Bit.SharedWeb.Swagger;
|
||||
|
||||
/// <summary>
|
||||
/// Adds the action name (function name) as an extension to each operation in the Swagger document.
|
||||
/// This can be useful for the code generation process, to generate more meaningful names for operations.
|
||||
/// Note that we add both the original action name and a snake_case version, as the codegen templates
|
||||
/// cannot do case conversions.
|
||||
/// </summary>
|
||||
public class ActionNameOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (!context.ApiDescription.ActionDescriptor.RouteValues.TryGetValue("action", out var action)) return;
|
||||
if (string.IsNullOrEmpty(action)) return;
|
||||
|
||||
operation.Extensions.Add("x-action-name", new OpenApiString(action));
|
||||
// We can't do case changes in the codegen templates, so we also add the snake_case version of the action name
|
||||
operation.Extensions.Add("x-action-name-snake-case", new OpenApiString(JsonNamingPolicy.SnakeCaseLower.ConvertName(action)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Bit.SharedWeb.Swagger;
|
||||
|
||||
/// <summary>
|
||||
/// Checks for duplicate operation IDs in the Swagger document, and throws an error if any are found.
|
||||
/// Operation IDs must be unique across the entire Swagger document according to the OpenAPI specification,
|
||||
/// but we use controller action names to generate them, which can lead to duplicates if a Controller function
|
||||
/// has multiple HTTP methods or if a Controller has overloaded functions.
|
||||
/// </summary>
|
||||
public class CheckDuplicateOperationIdsDocumentFilter(bool printDuplicates = true) : IDocumentFilter
|
||||
{
|
||||
public bool PrintDuplicates { get; } = printDuplicates;
|
||||
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
var operationIdMap = new Dictionary<string, List<(string Path, OpenApiPathItem PathItem, OperationType Method, OpenApiOperation Operation)>>();
|
||||
|
||||
foreach (var (path, pathItem) in swaggerDoc.Paths)
|
||||
{
|
||||
foreach (var operation in pathItem.Operations)
|
||||
{
|
||||
if (!operationIdMap.TryGetValue(operation.Value.OperationId, out var list))
|
||||
{
|
||||
list = [];
|
||||
operationIdMap[operation.Value.OperationId] = list;
|
||||
}
|
||||
|
||||
list.Add((path, pathItem, operation.Key, operation.Value));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Find duplicates
|
||||
var duplicates = operationIdMap.Where((kvp) => kvp.Value.Count > 1).ToList();
|
||||
if (duplicates.Count > 0)
|
||||
{
|
||||
if (PrintDuplicates)
|
||||
{
|
||||
Console.WriteLine($"\n######## Duplicate operationIds found in the schema ({duplicates.Count} found) ########\n");
|
||||
|
||||
Console.WriteLine("## Common causes of duplicate operation IDs:");
|
||||
Console.WriteLine("- Multiple HTTP methods (GET, POST, etc.) on the same controller function");
|
||||
Console.WriteLine(" Solution: Split the methods into separate functions, and if appropiate, mark the deprecated ones with [Obsolete]");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("- Overloaded controller functions with the same name");
|
||||
Console.WriteLine(" Solution: Rename the overloaded functions to have unique names, or combine them into a single function with optional parameters");
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("## The duplicate operation IDs are:");
|
||||
|
||||
foreach (var (operationId, duplicate) in duplicates)
|
||||
{
|
||||
Console.WriteLine($"- operationId: {operationId}");
|
||||
foreach (var (path, pathItem, method, operation) in duplicate)
|
||||
{
|
||||
Console.Write($" {method.ToString().ToUpper()} {path}");
|
||||
|
||||
|
||||
if (operation.Extensions.TryGetValue("x-source-file", out var sourceFile) && operation.Extensions.TryGetValue("x-source-line", out var sourceLine))
|
||||
{
|
||||
var sourceFileString = ((Microsoft.OpenApi.Any.OpenApiString)sourceFile).Value;
|
||||
var sourceLineString = ((Microsoft.OpenApi.Any.OpenApiInteger)sourceLine).Value;
|
||||
|
||||
Console.WriteLine($" {sourceFileString}:{sourceLineString}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
Console.WriteLine("\n");
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Duplicate operation IDs found in Swagger schema");
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs
Normal file
33
src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Bit.SharedWeb.Swagger;
|
||||
|
||||
public static class SwaggerGenOptionsExt
|
||||
{
|
||||
|
||||
public static void InitializeSwaggerFilters(
|
||||
this SwaggerGenOptions config, IWebHostEnvironment environment)
|
||||
{
|
||||
config.SchemaFilter<EnumSchemaFilter>();
|
||||
config.SchemaFilter<EncryptedStringSchemaFilter>();
|
||||
|
||||
config.OperationFilter<ActionNameOperationFilter>();
|
||||
|
||||
// Set the operation ID to the name of the controller followed by the name of the function.
|
||||
// Note that the "Controller" suffix for the controllers, and the "Async" suffix for the actions
|
||||
// are removed already, so we don't need to do that ourselves.
|
||||
// TODO(Dani): This is disabled until we remove all the duplicate operation IDs.
|
||||
// config.CustomOperationIds(e => $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.RouteValues["action"]}");
|
||||
// config.DocumentFilter<CheckDuplicateOperationIdsDocumentFilter>();
|
||||
|
||||
// These two filters require debug symbols/git, so only add them in development mode
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
config.DocumentFilter<GitCommitDocumentFilter>();
|
||||
config.OperationFilter<SourceFileLineOperationFilter>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ public class AuthRequestsControllerTests
|
||||
.Returns([authRequest]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Get();
|
||||
var result = await sutProvider.Sut.GetAll();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
|
||||
@@ -73,7 +73,7 @@ public class DevicesControllerTest
|
||||
_deviceRepositoryMock.GetManyByUserIdWithDeviceAuth(userId).Returns(devicesWithPendingAuthData);
|
||||
|
||||
// Act
|
||||
var result = await _sut.Get();
|
||||
var result = await _sut.GetAll();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
@@ -94,6 +94,6 @@ public class DevicesControllerTest
|
||||
_userServiceMock.GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>()).Returns((Guid?)null);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.Get());
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.GetAll());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ public class CollectionsControllerTests
|
||||
.GetManySharedCollectionsByOrganizationIdAsync(organization.Id)
|
||||
.Returns(collections);
|
||||
|
||||
var response = await sutProvider.Sut.Get(organization.Id);
|
||||
var response = await sutProvider.Sut.GetAll(organization.Id);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManySharedCollectionsByOrganizationIdAsync(organization.Id);
|
||||
|
||||
@@ -219,7 +219,7 @@ public class CollectionsControllerTests
|
||||
.GetManyByUserIdAsync(userId)
|
||||
.Returns(collections);
|
||||
|
||||
var result = await sutProvider.Sut.Get(organization.Id);
|
||||
var result = await sutProvider.Sut.GetAll(organization.Id);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceive().GetManyByOrganizationIdAsync(organization.Id);
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(userId);
|
||||
|
||||
67
test/SharedWeb.Test/ActionNameOperationFilterTest.cs
Normal file
67
test/SharedWeb.Test/ActionNameOperationFilterTest.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using Bit.SharedWeb.Swagger;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace SharedWeb.Test;
|
||||
|
||||
public class ActionNameOperationFilterTest
|
||||
{
|
||||
[Fact]
|
||||
public void WithValidActionNameAddsActionNameExtensions()
|
||||
{
|
||||
// Arrange
|
||||
var operation = new OpenApiOperation();
|
||||
var actionDescriptor = new ActionDescriptor();
|
||||
actionDescriptor.RouteValues["action"] = "GetUsers";
|
||||
|
||||
var apiDescription = new ApiDescription
|
||||
{
|
||||
ActionDescriptor = actionDescriptor
|
||||
};
|
||||
|
||||
var context = new OperationFilterContext(apiDescription, null, null, null);
|
||||
var filter = new ActionNameOperationFilter();
|
||||
|
||||
// Act
|
||||
filter.Apply(operation, context);
|
||||
|
||||
// Assert
|
||||
Assert.True(operation.Extensions.ContainsKey("x-action-name"));
|
||||
Assert.True(operation.Extensions.ContainsKey("x-action-name-snake-case"));
|
||||
|
||||
var actionNameExt = operation.Extensions["x-action-name"] as OpenApiString;
|
||||
var actionNameSnakeCaseExt = operation.Extensions["x-action-name-snake-case"] as OpenApiString;
|
||||
|
||||
Assert.NotNull(actionNameExt);
|
||||
Assert.NotNull(actionNameSnakeCaseExt);
|
||||
Assert.Equal("GetUsers", actionNameExt.Value);
|
||||
Assert.Equal("get_users", actionNameSnakeCaseExt.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithMissingActionRouteValueDoesNotAddExtensions()
|
||||
{
|
||||
// Arrange
|
||||
var operation = new OpenApiOperation();
|
||||
var actionDescriptor = new ActionDescriptor();
|
||||
// Not setting the "action" route value at all
|
||||
|
||||
var apiDescription = new ApiDescription
|
||||
{
|
||||
ActionDescriptor = actionDescriptor
|
||||
};
|
||||
|
||||
var context = new OperationFilterContext(apiDescription, null, null, null);
|
||||
var filter = new ActionNameOperationFilter();
|
||||
|
||||
// Act
|
||||
filter.Apply(operation, context);
|
||||
|
||||
// Assert
|
||||
Assert.False(operation.Extensions.ContainsKey("x-action-name"));
|
||||
Assert.False(operation.Extensions.ContainsKey("x-action-name-snake-case"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using Bit.SharedWeb.Swagger;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace SharedWeb.Test;
|
||||
|
||||
public class UniqueOperationIdsController : ControllerBase
|
||||
{
|
||||
[HttpGet("unique-get")]
|
||||
public void UniqueGetAction() { }
|
||||
|
||||
[HttpPost("unique-post")]
|
||||
public void UniquePostAction() { }
|
||||
}
|
||||
|
||||
public class OverloadedOperationIdsController : ControllerBase
|
||||
{
|
||||
[HttpPut("another-duplicate")]
|
||||
public void AnotherDuplicateAction() { }
|
||||
|
||||
[HttpPatch("another-duplicate/{id}")]
|
||||
public void AnotherDuplicateAction(int id) { }
|
||||
}
|
||||
|
||||
public class MultipleHttpMethodsController : ControllerBase
|
||||
{
|
||||
[HttpGet("multi-method")]
|
||||
[HttpPost("multi-method")]
|
||||
[HttpPut("multi-method")]
|
||||
public void MultiMethodAction() { }
|
||||
}
|
||||
|
||||
public class CheckDuplicateOperationIdsDocumentFilterTest
|
||||
{
|
||||
[Fact]
|
||||
public void UniqueOperationIdsDoNotThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var (swaggerDoc, context) = SwaggerDocUtil.CreateDocFromControllers(typeof(UniqueOperationIdsController));
|
||||
var filter = new CheckDuplicateOperationIdsDocumentFilter();
|
||||
filter.Apply(swaggerDoc, context);
|
||||
// Act & Assert
|
||||
var exception = Record.Exception(() => filter.Apply(swaggerDoc, context));
|
||||
Assert.Null(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DuplicateOperationIdsThrowInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var (swaggerDoc, context) = SwaggerDocUtil.CreateDocFromControllers(typeof(OverloadedOperationIdsController));
|
||||
var filter = new CheckDuplicateOperationIdsDocumentFilter(false);
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => filter.Apply(swaggerDoc, context));
|
||||
Assert.Contains("Duplicate operation IDs found in Swagger schema", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleHttpMethodsThrowInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var (swaggerDoc, context) = SwaggerDocUtil.CreateDocFromControllers(typeof(MultipleHttpMethodsController));
|
||||
var filter = new CheckDuplicateOperationIdsDocumentFilter(false);
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => filter.Apply(swaggerDoc, context));
|
||||
Assert.Contains("Duplicate operation IDs found in Swagger schema", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptySwaggerDocDoesNotThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var swaggerDoc = new OpenApiDocument { Paths = [] };
|
||||
var context = new DocumentFilterContext([], null, null);
|
||||
var filter = new CheckDuplicateOperationIdsDocumentFilter(false);
|
||||
|
||||
// Act & Assert
|
||||
var exception = Record.Exception(() => filter.Apply(swaggerDoc, context));
|
||||
Assert.Null(exception);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||
<PackageReference Include="xunit.runner.visualstudio"
|
||||
Version="$(XUnitRunnerVisualStudioVersion)">
|
||||
|
||||
85
test/SharedWeb.Test/SwaggerDocUtil.cs
Normal file
85
test/SharedWeb.Test/SwaggerDocUtil.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NSubstitute;
|
||||
using Swashbuckle.AspNetCore.Swagger;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace SharedWeb.Test;
|
||||
|
||||
public class SwaggerDocUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an OpenApiDocument and DocumentFilterContext from the specified controller type by setting up
|
||||
/// a minimal service collection and using the SwaggerProvider to generate the document.
|
||||
/// </summary>
|
||||
public static (OpenApiDocument, DocumentFilterContext) CreateDocFromControllers(params Type[] controllerTypes)
|
||||
{
|
||||
if (controllerTypes.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("At least one controller type must be provided", nameof(controllerTypes));
|
||||
}
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddSingleton(Substitute.For<IWebHostEnvironment>());
|
||||
services.AddControllers()
|
||||
.ConfigureApplicationPartManager(manager =>
|
||||
{
|
||||
// Clear existing parts and feature providers
|
||||
manager.ApplicationParts.Clear();
|
||||
manager.FeatureProviders.Clear();
|
||||
|
||||
// Add a custom feature provider that only includes the specific controller types
|
||||
manager.FeatureProviders.Add(new MultipleControllerFeatureProvider(controllerTypes));
|
||||
|
||||
// Add assembly parts for all unique assemblies containing the controllers
|
||||
foreach (var assembly in controllerTypes.Select(t => t.Assembly).Distinct())
|
||||
{
|
||||
manager.ApplicationParts.Add(new AssemblyPart(assembly));
|
||||
}
|
||||
});
|
||||
services.AddSwaggerGen(config =>
|
||||
{
|
||||
config.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "v1" });
|
||||
config.CustomOperationIds(e => $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.RouteValues["action"]}");
|
||||
});
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Get API descriptions
|
||||
var allApiDescriptions = serviceProvider.GetRequiredService<IApiDescriptionGroupCollectionProvider>()
|
||||
.ApiDescriptionGroups.Items
|
||||
.SelectMany(group => group.Items)
|
||||
.ToList();
|
||||
|
||||
if (allApiDescriptions.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No API descriptions found for controller, ensure your controllers are defined correctly (public, not nested, inherit from ControllerBase, etc.)");
|
||||
}
|
||||
|
||||
// Generate the swagger document and context
|
||||
var document = serviceProvider.GetRequiredService<ISwaggerProvider>().GetSwagger("v1");
|
||||
var schemaGenerator = serviceProvider.GetRequiredService<ISchemaGenerator>();
|
||||
var context = new DocumentFilterContext(allApiDescriptions, schemaGenerator, new SchemaRepository());
|
||||
|
||||
return (document, context);
|
||||
}
|
||||
}
|
||||
|
||||
public class MultipleControllerFeatureProvider(params Type[] controllerTypes) : ControllerFeatureProvider
|
||||
{
|
||||
private readonly HashSet<Type> _allowedControllerTypes = [.. controllerTypes];
|
||||
|
||||
protected override bool IsController(TypeInfo typeInfo)
|
||||
{
|
||||
return _allowedControllerTypes.Contains(typeInfo.AsType())
|
||||
&& typeInfo.IsClass
|
||||
&& !typeInfo.IsAbstract
|
||||
&& typeof(ControllerBase).IsAssignableFrom(typeInfo);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user